From 6467d3b58e29c15157be6b264a55acefeb89f406 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 6 May 2019 18:27:05 +0300 Subject: [PATCH 001/134] check python version --- freqtrade/main.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 877e2921d..79d150441 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -22,21 +22,28 @@ def main(sysargv: List[str]) -> None: This function will initiate the bot and start the trading loop. :return: None """ - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args: Namespace = arguments.get_parsed_arg() - - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot - if hasattr(args, 'func'): - args.func(args) - return - - worker = None - return_code = 1 try: + worker = None + return_code = 1 + + # check min. python version + if sys.version_info < (3, 6): + raise SystemError("Freqtrade requires Python version >= 3.6") + + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() + + # A subcommand has been issued. + # Means if Backtesting or Hyperopt have been called we exit the bot + if hasattr(args, 'func'): + args.func(args) + # TODO: fetch return_code as returned by the command function here + return_code = 0 + return + # Load and run worker worker = Worker(args) worker.run() @@ -47,8 +54,8 @@ def main(sysargv: List[str]) -> None: except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: - logger.exception('Fatal exception!') + except BaseException as e: + logger.exception('Fatal exception! ' + str(e)) finally: if worker: worker.exit() From c3c745ca19f0bb2885bae9fa1858c731564498a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:08:24 +0200 Subject: [PATCH 002/134] Get new files from old branch --- freqtrade/rpc/api_server.py | 203 +++++++++++++++++++++++++++++ freqtrade/rpc/api_server_common.py | 74 +++++++++++ freqtrade/rpc/rest_client.py | 47 +++++++ 3 files changed, 324 insertions(+) create mode 100644 freqtrade/rpc/api_server.py create mode 100644 freqtrade/rpc/api_server_common.py create mode 100755 freqtrade/rpc/rest_client.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py new file mode 100644 index 000000000..1055a0553 --- /dev/null +++ b/freqtrade/rpc/api_server.py @@ -0,0 +1,203 @@ +import json +import threading +import logging +# import json +from typing import Dict + +from flask import Flask, request +# from flask_restful import Resource, Api +from freqtrade.rpc.rpc import RPC, RPCException +from ipaddress import IPv4Address + + +logger = logging.getLogger(__name__) +app = Flask(__name__) + + +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 + """ + + def __init__(self, freqtrade) -> None: + """ + Init the api server, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + + self._config = freqtrade.config + + # Register application handling + self.register_rest_other() + self.register_rest_rpc_urls() + + thread = threading.Thread(target=self.run, daemon=True) + thread.start() + + def register_rest_other(self): + """ + Registers flask app URLs that are not calls to functionality in rpc.rpc. + :return: + """ + app.register_error_handler(404, self.page_not_found) + app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + + def register_rest_rpc_urls(self): + """ + Registers flask app URLs that are calls to functonality in rpc.rpc. + + First two arguments passed are /URL and 'Label' + Label can be used as a shortcut when refactoring + :return: + """ + app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) + app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) + app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) + app.add_url_rule('/status_table', 'status_table', + view_func=self.status_table, methods=['GET']) + + def run(self): + """ Method that runs flask app in its own thread forever """ + + """ + Section to handle configuration and running of the Rest server + also to check and warn if not bound to a loopback, warn on security risk. + """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] + + logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) + if not IPv4Address(rest_ip).is_loopback: + logger.info("SECURITY WARNING - Local Rest Server listening to external connections") + logger.info("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + # Run the Server + logger.info('Starting Local Rest Server') + try: + app.run(host=rest_ip, port=rest_port) + except Exception: + logger.exception("Api server failed to start, exception message is:") + + def cleanup(self) -> None: + pass + + def send_msg(self, msg: Dict[str, str]) -> None: + pass + + """ + Define the application methods here, called by app.add_url_rule + each Telegram command should have a like local substitute + """ + + def page_not_found(self, error): + """ + Return "404 not found", 404. + """ + return json.dumps({ + 'status': 'error', + 'reason': '''There's no API call for %s''' % request.base_url, + 'code': 404 + }), 404 + + def hello(self): + """ + None critical but helpful default index page. + + That lists URLs added to the flask server. + This may be deprecated at any time. + :return: index.html + """ + rest_cmds = 'Commands implemented:
' \ + 'Show 7 days of stats' \ + '
' \ + 'Stop the Trade thread' \ + '
' \ + 'Start the Traded thread' \ + '
' \ + 'Show profit summary' \ + '
' \ + 'Show status table - Open trades' \ + '
' \ + ' 404 page does not exist' \ + '
' + + return rest_cmds + + def daily(self): + """ + Returns the last X days trading stats summary. + + :return: stats + """ + try: + timescale = request.args.get('timescale') + logger.info("LocalRPC - Daily Command Called") + timescale = int(timescale) + + stats = self._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return json.dumps(stats, indent=4, sort_keys=True, default=str) + except RPCException as e: + logger.exception("API Error querying daily:", e) + return "Error querying daily" + + def profit(self): + """ + Handler for /profit. + + Returns a cumulative profit statistics + :return: stats + """ + try: + logger.info("LocalRPC - Profit Command Called") + + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return json.dumps(stats, indent=4, sort_keys=True, default=str) + except RPCException as e: + logger.exception("API Error calling profit", e) + return "Error querying closed trades - maybe there are none" + + def status_table(self): + """ + Handler for /status table. + + Returns the current TradeThread status in table format + :return: results + """ + try: + results = self._rpc_trade_status() + return json.dumps(results, indent=4, sort_keys=True, default=str) + + except RPCException as e: + logger.exception("API Error calling status table", e) + return "Error querying open trades - maybe there are none." + + def start(self): + """ + Handler for /start. + + Starts TradeThread in bot if stopped. + """ + msg = self._rpc_start() + return json.dumps(msg) + + def stop(self): + """ + Handler for /stop. + + Stops TradeThread in bot if running + """ + msg = self._rpc_stop() + return json.dumps(msg) diff --git a/freqtrade/rpc/api_server_common.py b/freqtrade/rpc/api_server_common.py new file mode 100644 index 000000000..19338a825 --- /dev/null +++ b/freqtrade/rpc/api_server_common.py @@ -0,0 +1,74 @@ +import logging +import flask +from flask import request, jsonify + +logger = logging.getLogger(__name__) + + +class MyApiApp(flask.Flask): + def __init__(self, import_name): + """ + Contains common rest routes and resource that do not need + to access to rpc.rpc functionality + """ + super(MyApiApp, self).__init__(import_name) + + """ + Registers flask app URLs that are not calls to functionality in rpc.rpc. + :return: + """ + self.before_request(self.my_preprocessing) + self.register_error_handler(404, self.page_not_found) + self.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + self.add_url_rule('/stop_api', 'stop_api', view_func=self.stop_api, methods=['GET']) + + def my_preprocessing(self): + # Do stuff to flask.request + pass + + def page_not_found(self, error): + # Return "404 not found", 404. + return jsonify({'status': 'error', + 'reason': '''There's no API call for %s''' % request.base_url, + 'code': 404}), 404 + + def hello(self): + """ + None critical but helpful default index page. + + That lists URLs added to the flask server. + This may be deprecated at any time. + :return: index.html + """ + rest_cmds = 'Commands implemented:
' \ + 'Show 7 days of stats' \ + '
' \ + 'Stop the Trade thread' \ + '
' \ + 'Start the Traded thread' \ + '
' \ + ' 404 page does not exist' \ + '
' \ + '
' \ + 'Shut down the api server - be sure' + return rest_cmds + + def stop_api(self): + """ For calling shutdown_api_server over via api server HTTP""" + self.shutdown_api_server() + return 'Api Server shutting down... ' + + def shutdown_api_server(self): + """ + Stop the running flask application + + Records the shutdown in logger.info + :return: + """ + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running the Flask Werkzeug Server') + if func is not None: + logger.info('Stopping the Local Rest Server') + func() + return diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py new file mode 100755 index 000000000..cabedebb8 --- /dev/null +++ b/freqtrade/rpc/rest_client.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Simple command line client into RPC commands +Can be used as an alternate to Telegram +""" + +import time +from requests import get +from sys import argv + +# TODO - use argparse to clean this up +# TODO - use IP and Port from config.json not hardcode + +if len(argv) == 1: + print('\nThis script accepts the following arguments') + print('- daily (int) - Where int is the number of days to report back. daily 3') + print('- start - this will start the trading thread') + print('- stop - this will start the trading thread') + print('- there will be more....\n') + +if len(argv) == 3 and argv[1] == "daily": + if str.isnumeric(argv[2]): + get_url = 'http://localhost:5002/daily?timescale=' + argv[2] + d = get(get_url).json() + print(d) + else: + print("\nThe second argument to daily must be an integer, 1,2,3 etc") + +if len(argv) == 2 and argv[1] == "start": + get_url = 'http://localhost:5002/start' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) + +if len(argv) == 2 and argv[1] == "stop": + get_url = 'http://localhost:5002/stop' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) From 26c42bd5590e70356e59963425c990cf0c379198 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:12:58 +0200 Subject: [PATCH 003/134] Add apiserver tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 freqtrade/tests/rpc/test_rpc_apiserver.py diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py new file mode 100644 index 000000000..14c35a38e --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -0,0 +1,52 @@ +""" +Unit test file for rpc/api_server.py +""" + +from unittest.mock import MagicMock + +from freqtrade.rpc.api_server import ApiServer +from freqtrade.state import State +from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver + + +def test__init__(default_conf, mocker): + """ + Test __init__() method + """ + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + assert apiserver._config == default_conf + + +def test_start_endpoint(default_conf, mocker): + """Test /start endpoint""" + patch_apiserver(mocker) + bot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(bot) + + bot.state = State.STOPPED + assert bot.state == State.STOPPED + result = apiserver.start() + assert result == '{"status": "starting trader ..."}' + assert bot.state == State.RUNNING + + result = apiserver.start() + assert result == '{"status": "already running"}' + + +def test_stop_endpoint(default_conf, mocker): + """Test /stop endpoint""" + patch_apiserver(mocker) + bot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(bot) + + bot.state = State.RUNNING + assert bot.state == State.RUNNING + result = apiserver.stop() + assert result == '{"status": "stopping trader ..."}' + assert bot.state == State.STOPPED + + result = apiserver.stop() + assert result == '{"status": "already stopped"}' From 6f67ea44dce5e81b3ebf3cd5e29f3d5e82459725 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:14 +0200 Subject: [PATCH 004/134] Enable config-check for rest server --- freqtrade/constants.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 619508e73..1b06eb726 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,19 @@ CONF_SCHEMA = { 'webhookstatus': {'type': 'object'}, }, }, + 'api_server': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'listen_ip_address': {'format': 'ipv4'}, + 'listen_port': { + 'type': 'integer', + "minimum": 1024, + "maximum": 65535 + }, + }, + 'required': ['enabled', 'listen_ip_address', 'listen_port'] + }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'forcebuy_enable': {'type': 'boolean'}, From ef2950bca2210dd2873534d62ba02a19b89f9b63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:40 +0200 Subject: [PATCH 005/134] Load api-server in rpc_manager --- freqtrade/rpc/rpc.py | 5 +++++ freqtrade/rpc/rpc_manager.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2189a0d17..5b78c8356 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -48,6 +48,11 @@ class RPCException(Exception): def __str__(self): return self.message + def __json__(self): + return { + 'msg': self.message + } + class RPC(object): """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7f0d0a5d4..fad532aa0 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -29,6 +29,12 @@ class RPCManager(object): from freqtrade.rpc.webhook import Webhook self.registered_modules.append(Webhook(freqtrade)) + # Enable local rest api server for cmd line control + if freqtrade.config.get('api_server', {}).get('enabled', False): + logger.info('Enabling rpc.api_server') + from freqtrade.rpc.api_server import ApiServer + self.registered_modules.append(ApiServer(freqtrade)) + def cleanup(self) -> None: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') From 68743012e400cde9e04a434e385eeceeded237c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 07:13:55 +0200 Subject: [PATCH 006/134] Patch api server for tests --- freqtrade/tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0bff1d5e9..98563a374 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -134,6 +134,15 @@ def patch_coinmarketcap(mocker) -> None: ) +def patch_apiserver(mocker) -> None: + mocker.patch.multiple( + 'freqtrade.rpc.api_server.ApiServer', + run=MagicMock(), + register_rest_other=MagicMock(), + register_rest_rpc_urls=MagicMock(), + ) + + @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ From 9d95ae934190ce72435dae7f1ce819de77f0bc3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 19:34:05 +0200 Subject: [PATCH 007/134] Add flask to dependencies --- requirements-common.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-common.txt b/requirements-common.txt index 4f7309a6a..9e3e2816c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -27,3 +27,6 @@ python-rapidjson==0.7.1 # Notify systemd sdnotify==0.3.2 + +# Api server +flask==1.0.2 From 6bb2fad9b07e2ffd64c2e3b22a9ab77f5f815224 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 19:34:19 +0200 Subject: [PATCH 008/134] Reorder some things --- freqtrade/rpc/api_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 1055a0553..46678466d 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -38,6 +38,12 @@ class ApiServer(RPC): thread = threading.Thread(target=self.run, daemon=True) thread.start() + def cleanup(self) -> None: + pass + + def send_msg(self, msg: Dict[str, str]) -> None: + pass + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -84,12 +90,6 @@ class ApiServer(RPC): except Exception: logger.exception("Api server failed to start, exception message is:") - def cleanup(self) -> None: - pass - - def send_msg(self, msg: Dict[str, str]) -> None: - pass - """ Define the application methods here, called by app.add_url_rule each Telegram command should have a like local substitute From 96a260b027075809d4fc09e351a5f1a4733d36bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 20:21:26 +0200 Subject: [PATCH 009/134] rest_dump --- freqtrade/rpc/api_server.py | 43 +++++++++-------- freqtrade/rpc/api_server_common.py | 74 ------------------------------ 2 files changed, 21 insertions(+), 96 deletions(-) delete mode 100644 freqtrade/rpc/api_server_common.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 46678466d..a0532a3b3 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -4,7 +4,7 @@ import logging # import json from typing import Dict -from flask import Flask, request +from flask import Flask, request, jsonify # from flask_restful import Resource, Api from freqtrade.rpc.rpc import RPC, RPCException from ipaddress import IPv4Address @@ -39,11 +39,15 @@ class ApiServer(RPC): thread.start() def cleanup(self) -> None: - pass + logger.info("Stopping API Server") def send_msg(self, msg: Dict[str, str]) -> None: pass + def rest_dump(self, return_value): + """ Helper function to jsonify object for a webserver """ + return jsonify(return_value) + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -89,6 +93,7 @@ class ApiServer(RPC): app.run(host=rest_ip, port=rest_port) except Exception: logger.exception("Api server failed to start, exception message is:") + logger.info('Starting Local Rest Server_end') """ Define the application methods here, called by app.add_url_rule @@ -99,7 +104,7 @@ class ApiServer(RPC): """ Return "404 not found", 404. """ - return json.dumps({ + return self.rest_dump({ 'status': 'error', 'reason': '''There's no API call for %s''' % request.base_url, 'code': 404 @@ -113,20 +118,14 @@ class ApiServer(RPC): This may be deprecated at any time. :return: index.html """ - rest_cmds = 'Commands implemented:
' \ - 'Show 7 days of stats' \ - '
' \ - 'Stop the Trade thread' \ - '
' \ - 'Start the Traded thread' \ - '
' \ - 'Show profit summary' \ - '
' \ - 'Show status table - Open trades' \ - '
' \ - ' 404 page does not exist' \ - '
' - + rest_cmds = ('Commands implemented:
' + 'Show 7 days of stats
' + 'Stop the Trade thread
' + 'Start the Traded thread
' + 'Show profit summary
' + 'Show status table - Open trades
' + ' 404 page does not exist
' + ) return rest_cmds def daily(self): @@ -145,7 +144,7 @@ class ApiServer(RPC): self._config['fiat_display_currency'] ) - return json.dumps(stats, indent=4, sort_keys=True, default=str) + return self.rest_dump(stats) except RPCException as e: logger.exception("API Error querying daily:", e) return "Error querying daily" @@ -164,7 +163,7 @@ class ApiServer(RPC): self._config['fiat_display_currency'] ) - return json.dumps(stats, indent=4, sort_keys=True, default=str) + return self.rest_dump(stats) except RPCException as e: logger.exception("API Error calling profit", e) return "Error querying closed trades - maybe there are none" @@ -178,7 +177,7 @@ class ApiServer(RPC): """ try: results = self._rpc_trade_status() - return json.dumps(results, indent=4, sort_keys=True, default=str) + return self.rest_dump(results) except RPCException as e: logger.exception("API Error calling status table", e) @@ -191,7 +190,7 @@ class ApiServer(RPC): Starts TradeThread in bot if stopped. """ msg = self._rpc_start() - return json.dumps(msg) + return self.rest_dump(msg) def stop(self): """ @@ -200,4 +199,4 @@ class ApiServer(RPC): Stops TradeThread in bot if running """ msg = self._rpc_stop() - return json.dumps(msg) + return self.rest_dump(msg) diff --git a/freqtrade/rpc/api_server_common.py b/freqtrade/rpc/api_server_common.py deleted file mode 100644 index 19338a825..000000000 --- a/freqtrade/rpc/api_server_common.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -import flask -from flask import request, jsonify - -logger = logging.getLogger(__name__) - - -class MyApiApp(flask.Flask): - def __init__(self, import_name): - """ - Contains common rest routes and resource that do not need - to access to rpc.rpc functionality - """ - super(MyApiApp, self).__init__(import_name) - - """ - Registers flask app URLs that are not calls to functionality in rpc.rpc. - :return: - """ - self.before_request(self.my_preprocessing) - self.register_error_handler(404, self.page_not_found) - self.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) - self.add_url_rule('/stop_api', 'stop_api', view_func=self.stop_api, methods=['GET']) - - def my_preprocessing(self): - # Do stuff to flask.request - pass - - def page_not_found(self, error): - # Return "404 not found", 404. - return jsonify({'status': 'error', - 'reason': '''There's no API call for %s''' % request.base_url, - 'code': 404}), 404 - - def hello(self): - """ - None critical but helpful default index page. - - That lists URLs added to the flask server. - This may be deprecated at any time. - :return: index.html - """ - rest_cmds = 'Commands implemented:
' \ - 'Show 7 days of stats' \ - '
' \ - 'Stop the Trade thread' \ - '
' \ - 'Start the Traded thread' \ - '
' \ - ' 404 page does not exist' \ - '
' \ - '
' \ - 'Shut down the api server - be sure' - return rest_cmds - - def stop_api(self): - """ For calling shutdown_api_server over via api server HTTP""" - self.shutdown_api_server() - return 'Api Server shutting down... ' - - def shutdown_api_server(self): - """ - Stop the running flask application - - Records the shutdown in logger.info - :return: - """ - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running the Flask Werkzeug Server') - if func is not None: - logger.info('Stopping the Local Rest Server') - func() - return From c6c2893e2cef4a058a4d5adc2c4e13755e44d876 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Apr 2019 21:07:44 +0200 Subject: [PATCH 010/134] Improve rest-client interface --- freqtrade/rpc/rest_client.py | 96 ++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index cabedebb8..5ae65dc4a 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -2,46 +2,80 @@ """ Simple command line client into RPC commands Can be used as an alternate to Telegram + +Should not import anything from freqtrade, +so it can be used as a standalone script. """ +import argparse +import logging import time -from requests import get from sys import argv -# TODO - use argparse to clean this up +import click + +from requests import get +from requests.exceptions import ConnectionError + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) +logger = logging.getLogger("ft_rest_client") + # TODO - use IP and Port from config.json not hardcode -if len(argv) == 1: - print('\nThis script accepts the following arguments') - print('- daily (int) - Where int is the number of days to report back. daily 3') - print('- start - this will start the trading thread') - print('- stop - this will start the trading thread') - print('- there will be more....\n') +COMMANDS_NO_ARGS = ["start", + "stop", + ] +COMMANDS_ARGS = ["daily", + ] -if len(argv) == 3 and argv[1] == "daily": - if str.isnumeric(argv[2]): - get_url = 'http://localhost:5002/daily?timescale=' + argv[2] - d = get(get_url).json() - print(d) - else: - print("\nThe second argument to daily must be an integer, 1,2,3 etc") +SERVER_URL = "http://localhost:5002" -if len(argv) == 2 and argv[1] == "start": - get_url = 'http://localhost:5002/start' - d = get(get_url).text - print(d) - if "already" not in d: - time.sleep(2) - d = get(get_url).text - print(d) +def add_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("command", + help="Positional argument defining the command to execute.") + args = parser.parse_args() + # if len(argv) == 1: + # print('\nThis script accepts the following arguments') + # print('- daily (int) - Where int is the number of days to report back. daily 3') + # print('- start - this will start the trading thread') + # print('- stop - this will start the trading thread') + # print('- there will be more....\n') + return vars(args) -if len(argv) == 2 and argv[1] == "stop": - get_url = 'http://localhost:5002/stop' - d = get(get_url).text - print(d) - if "already" not in d: - time.sleep(2) - d = get(get_url).text - print(d) +def call_authorized(url): + try: + return get(url).json() + except ConnectionError: + logger.warning("Connection error") + + +def call_command_noargs(command): + logger.info(f"Running command `{command}` at {SERVER_URL}") + r = call_authorized(f"{SERVER_URL}/{command}") + logger.info(r) + + +def main(args): + + # Call commands without arguments + if args["command"] in COMMANDS_NO_ARGS: + call_command_noargs(args["command"]) + + if args["command"] == "daily": + if str.isnumeric(argv[2]): + get_url = SERVER_URL + '/daily?timescale=' + argv[2] + d = get(get_url).json() + print(d) + else: + print("\nThe second argument to daily must be an integer, 1,2,3 etc") + + +if __name__ == "__main__": + args = add_arguments() + main(args) From 8993882dcb1434b0f938ac45eee23cb4e4c5f915 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Apr 2019 06:39:33 +0200 Subject: [PATCH 011/134] Sort imports --- freqtrade/rpc/api_server.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a0532a3b3..20850a3a1 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,14 +1,11 @@ -import json -import threading import logging -# import json +import threading +from ipaddress import IPv4Address from typing import Dict -from flask import Flask, request, jsonify -# from flask_restful import Resource, Api -from freqtrade.rpc.rpc import RPC, RPCException -from ipaddress import IPv4Address +from flask import Flask, jsonify, request +from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) app = Flask(__name__) @@ -42,6 +39,7 @@ class ApiServer(RPC): logger.info("Stopping API Server") def send_msg(self, msg: Dict[str, str]) -> None: + """We don't push to endpoints at the moment. Look at webhooks for that.""" pass def rest_dump(self, return_value): From 3cf6c6ee0c4ed3c346b80983789ee2acbc192750 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 19:58:00 +0200 Subject: [PATCH 012/134] Implement a few more methods --- freqtrade/rpc/api_server.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 20850a3a1..53520025b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -37,6 +37,8 @@ class ApiServer(RPC): def cleanup(self) -> None: logger.info("Stopping API Server") + # TODO: Gracefully shutdown - right now it'll fail on /reload_conf + # since it's not terminated correctly. def send_msg(self, msg: Dict[str, str]) -> None: """We don't push to endpoints at the moment. Look at webhooks for that.""" @@ -62,8 +64,13 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ - app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + # TODO: actions should not be GET... app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) + app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self.stopbuy, methods=['GET']) + app.add_url_rule('/reload_conf', 'reload_conf', view_func=self.reload_conf, + methods=['GET']) + app.add_url_rule('/count', 'count', view_func=self.count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) app.add_url_rule('/status_table', 'status_table', @@ -198,3 +205,31 @@ class ApiServer(RPC): """ msg = self._rpc_stop() return self.rest_dump(msg) + + def stopbuy(self): + """ + Handler for /stopbuy. + + Sets max_open_trades to 0 and gracefully sells all open trades + """ + msg = self._rpc_stopbuy() + return self.rest_dump(msg) + + def reload_conf(self): + """ + Handler for /reload_conf. + Triggers a config file reload + """ + msg = self._rpc_reload_conf() + return self.rest_dump(msg) + + def count(self): + """ + Handler for /count. + Returns the number of trades running + """ + try: + msg = self._rpc_count() + except RPCException as e: + msg = {"status": str(e)} + return self.rest_dump(msg) From 2f8088432c9f16423ea7a3625cf5128a12b478a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2019 20:08:01 +0200 Subject: [PATCH 013/134] All handlers should be private --- freqtrade/rpc/api_server.py | 119 +++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 53520025b..30771bdc7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -5,6 +5,7 @@ from typing import Dict from flask import Flask, jsonify, request +from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -65,16 +66,17 @@ class ApiServer(RPC): :return: """ # TODO: actions should not be GET... - app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) - app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self.stopbuy, methods=['GET']) - app.add_url_rule('/reload_conf', 'reload_conf', view_func=self.reload_conf, + app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) + app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['GET']) - app.add_url_rule('/count', 'count', view_func=self.count, methods=['GET']) - app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) - app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) + app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) + app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status_table', 'status_table', - view_func=self.status_table, methods=['GET']) + view_func=self._status_table, methods=['GET']) def run(self): """ Method that runs flask app in its own thread forever """ @@ -133,7 +135,56 @@ class ApiServer(RPC): ) return rest_cmds - def daily(self): + def _start(self): + """ + Handler for /start. + Starts TradeThread in bot if stopped. + """ + msg = self._rpc_start() + return self.rest_dump(msg) + + def _stop(self): + """ + Handler for /stop. + Stops TradeThread in bot if running + """ + msg = self._rpc_stop() + return self.rest_dump(msg) + + def _stopbuy(self): + """ + Handler for /stopbuy. + Sets max_open_trades to 0 and gracefully sells all open trades + """ + msg = self._rpc_stopbuy() + return self.rest_dump(msg) + + def _version(self): + """ + Prints the bot's version + """ + return self.rest_dump({"version": __version__}) + + def _reload_conf(self): + """ + Handler for /reload_conf. + Triggers a config file reload + """ + msg = self._rpc_reload_conf() + return self.rest_dump(msg) + + def _count(self): + """ + Handler for /count. + Returns the number of trades running + """ + try: + msg = self._rpc_count() + except RPCException as e: + msg = {"status": str(e)} + return self.rest_dump(msg) + + def _daily(self): """ Returns the last X days trading stats summary. @@ -154,7 +205,7 @@ class ApiServer(RPC): logger.exception("API Error querying daily:", e) return "Error querying daily" - def profit(self): + def _profit(self): """ Handler for /profit. @@ -173,7 +224,7 @@ class ApiServer(RPC): logger.exception("API Error calling profit", e) return "Error querying closed trades - maybe there are none" - def status_table(self): + def _status_table(self): """ Handler for /status table. @@ -187,49 +238,3 @@ class ApiServer(RPC): except RPCException as e: logger.exception("API Error calling status table", e) return "Error querying open trades - maybe there are none." - - def start(self): - """ - Handler for /start. - - Starts TradeThread in bot if stopped. - """ - msg = self._rpc_start() - return self.rest_dump(msg) - - def stop(self): - """ - Handler for /stop. - - Stops TradeThread in bot if running - """ - msg = self._rpc_stop() - return self.rest_dump(msg) - - def stopbuy(self): - """ - Handler for /stopbuy. - - Sets max_open_trades to 0 and gracefully sells all open trades - """ - msg = self._rpc_stopbuy() - return self.rest_dump(msg) - - def reload_conf(self): - """ - Handler for /reload_conf. - Triggers a config file reload - """ - msg = self._rpc_reload_conf() - return self.rest_dump(msg) - - def count(self): - """ - Handler for /count. - Returns the number of trades running - """ - try: - msg = self._rpc_count() - except RPCException as e: - msg = {"status": str(e)} - return self.rest_dump(msg) From a12e093417b3bc60c7c20b6410798e4ef3e836be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Apr 2019 13:09:53 +0200 Subject: [PATCH 014/134] Api server - custom json encoder --- freqtrade/rpc/api_server.py | 50 ++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 30771bdc7..fe2367458 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -3,13 +3,31 @@ import threading from ipaddress import IPv4Address from typing import Dict +from arrow import Arrow from flask import Flask, jsonify, request +from flask.json import JSONEncoder from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) + + +class ArrowJSONEncoder(JSONEncoder): + def default(self, obj): + try: + if isinstance(obj, Arrow): + return obj.for_json() + iterable = iter(obj) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, obj) + + app = Flask(__name__) +app.json_encoder = ArrowJSONEncoder class ApiServer(RPC): @@ -42,13 +60,19 @@ class ApiServer(RPC): # since it's not terminated correctly. def send_msg(self, msg: Dict[str, str]) -> None: - """We don't push to endpoints at the moment. Look at webhooks for that.""" + """ + We don't push to endpoints at the moment. + Take a look at webhooks for that functionality. + """ pass def rest_dump(self, return_value): """ Helper function to jsonify object for a webserver """ return jsonify(return_value) + def rest_error(self, error_msg): + return jsonify({"error": error_msg}), 502 + def register_rest_other(self): """ Registers flask app URLs that are not calls to functionality in rpc.rpc. @@ -75,8 +99,7 @@ class ApiServer(RPC): app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/status_table', 'status_table', - view_func=self._status_table, methods=['GET']) + app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) def run(self): """ Method that runs flask app in its own thread forever """ @@ -180,9 +203,9 @@ class ApiServer(RPC): """ try: msg = self._rpc_count() + return self.rest_dump(msg) except RPCException as e: - msg = {"status": str(e)} - return self.rest_dump(msg) + return self.rest_error(str(e)) def _daily(self): """ @@ -202,8 +225,8 @@ class ApiServer(RPC): return self.rest_dump(stats) except RPCException as e: - logger.exception("API Error querying daily:", e) - return "Error querying daily" + logger.exception("API Error querying daily: %s", e) + return self.rest_error(f"Error querying daily {e}") def _profit(self): """ @@ -221,20 +244,19 @@ class ApiServer(RPC): return self.rest_dump(stats) except RPCException as e: - logger.exception("API Error calling profit", e) - return "Error querying closed trades - maybe there are none" + logger.exception("API Error calling profit: %s", e) + return self.rest_error("Error querying closed trades - maybe there are none") - def _status_table(self): + def _status(self): """ Handler for /status table. - Returns the current TradeThread status in table format - :return: results + Returns the current status of the trades in json format """ try: results = self._rpc_trade_status() return self.rest_dump(results) except RPCException as e: - logger.exception("API Error calling status table", e) - return "Error querying open trades - maybe there are none." + logger.exception("API Error calling status table: %s", e) + return self.rest_error("Error querying open trades - maybe there are none.") From d8549fe09ab9bd5f9410149abc4b3fd780615603 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Apr 2019 13:22:44 +0200 Subject: [PATCH 015/134] add balance handler --- freqtrade/rpc/api_server.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index fe2367458..a07057997 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,16 +90,27 @@ class ApiServer(RPC): :return: """ # TODO: actions should not be GET... + # Actions to control the bot app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['GET']) + # Info commands + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) + app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + # TODO: Implement the following + # performance + # forcebuy + # forcesell + # whitelist + # blacklist + # edge + # help (?) def run(self): """ Method that runs flask app in its own thread forever """ @@ -258,5 +269,19 @@ class ApiServer(RPC): return self.rest_dump(results) except RPCException as e: - logger.exception("API Error calling status table: %s", e) + logger.exception("API Error calling status: %s", e) return self.rest_error("Error querying open trades - maybe there are none.") + + def _balance(self): + """ + Handler for /balance table. + + Returns the current status of the trades in json format + """ + try: + results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + return self.rest_dump(results) + + except RPCException as e: + logger.exception("API Error calling status table: %s", e) + return self.rest_error(f"{e}") From 01c93a2ee36d878289c3e90fe38df4888098c8df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 06:40:15 +0200 Subject: [PATCH 016/134] Load rest-client config from file --- freqtrade/rpc/rest_client.py | 40 ++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 5ae65dc4a..51c7a88f5 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -8,11 +8,10 @@ so it can be used as a standalone script. """ import argparse +import json import logging -import time from sys import argv - -import click +from pathlib import Path from requests import get from requests.exceptions import ConnectionError @@ -23,21 +22,27 @@ logging.basicConfig( ) logger = logging.getLogger("ft_rest_client") -# TODO - use IP and Port from config.json not hardcode COMMANDS_NO_ARGS = ["start", "stop", + "stopbuy", + "reload_conf" ] COMMANDS_ARGS = ["daily", ] -SERVER_URL = "http://localhost:5002" - def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + parser.add_argument('-c', '--config', + help='Specify configuration file (default: %(default)s). ', + dest='config', + type=str, + metavar='PATH', + default='config.json' + ) args = parser.parse_args() # if len(argv) == 1: # print('\nThis script accepts the following arguments') @@ -48,6 +53,14 @@ def add_arguments(): return vars(args) +def load_config(configfile): + file = Path(configfile) + if file.is_file(): + with file.open("r") as f: + config = json.load(f) + return config + + def call_authorized(url): try: return get(url).json() @@ -55,21 +68,26 @@ def call_authorized(url): logger.warning("Connection error") -def call_command_noargs(command): - logger.info(f"Running command `{command}` at {SERVER_URL}") - r = call_authorized(f"{SERVER_URL}/{command}") +def call_command_noargs(server_url, command): + logger.info(f"Running command `{command}` at {server_url}") + r = call_authorized(f"{server_url}/{command}") logger.info(r) def main(args): + config = load_config(args["config"]) + url = config.get("api_server", {}).get("server_url", "127.0.0.1") + port = config.get("api_server", {}).get("listen_port", "8080") + server_url = f"http://{url}:{port}" + # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - call_command_noargs(args["command"]) + call_command_noargs(server_url, args["command"]) if args["command"] == "daily": if str.isnumeric(argv[2]): - get_url = SERVER_URL + '/daily?timescale=' + argv[2] + get_url = server_url + '/daily?timescale=' + argv[2] d = get(get_url).json() print(d) else: From ae8660fe06f213d7e5017d6bceb0de84b2811f33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 06:59:07 +0200 Subject: [PATCH 017/134] Extract exception handling to decorator --- freqtrade/rpc/api_server.py | 85 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a07057997..06f4ad0b9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -37,6 +37,18 @@ class ApiServer(RPC): This class starts a none blocking thread the api server runs within """ + def safe_rpc(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 __init__(self, freqtrade) -> None: """ Init the api server, and init the super class RPC @@ -103,11 +115,11 @@ class ApiServer(RPC): app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) # TODO: Implement the following # performance # forcebuy # forcesell - # whitelist # blacklist # edge # help (?) @@ -207,38 +219,34 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) + @safe_rpc def _count(self): """ Handler for /count. Returns the number of trades running """ - try: - msg = self._rpc_count() - return self.rest_dump(msg) - except RPCException as e: - return self.rest_error(str(e)) + msg = self._rpc_count() + return self.rest_dump(msg) + @safe_rpc def _daily(self): """ Returns the last X days trading stats summary. :return: stats """ - try: - timescale = request.args.get('timescale') - logger.info("LocalRPC - Daily Command Called") - timescale = int(timescale) + timescale = request.args.get('timescale') + logger.info("LocalRPC - Daily Command Called") + timescale = int(timescale) - stats = self._rpc_daily_profit(timescale, - self._config['stake_currency'], - self._config['fiat_display_currency'] - ) + stats = self._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) - return self.rest_dump(stats) - except RPCException as e: - logger.exception("API Error querying daily: %s", e) - return self.rest_error(f"Error querying daily {e}") + return self.rest_dump(stats) + @safe_rpc def _profit(self): """ Handler for /profit. @@ -246,42 +254,39 @@ class ApiServer(RPC): Returns a cumulative profit statistics :return: stats """ - try: - logger.info("LocalRPC - Profit Command Called") + logger.info("LocalRPC - Profit Command Called") - stats = self._rpc_trade_statistics(self._config['stake_currency'], - self._config['fiat_display_currency'] - ) + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) - return self.rest_dump(stats) - except RPCException as e: - logger.exception("API Error calling profit: %s", e) - return self.rest_error("Error querying closed trades - maybe there are none") + return self.rest_dump(stats) + @safe_rpc def _status(self): """ Handler for /status table. Returns the current status of the trades in json format """ - try: - results = self._rpc_trade_status() - return self.rest_dump(results) - - except RPCException as e: - logger.exception("API Error calling status: %s", e) - return self.rest_error("Error querying open trades - maybe there are none.") + results = self._rpc_trade_status() + return self.rest_dump(results) + @safe_rpc def _balance(self): """ Handler for /balance table. Returns the current status of the trades in json format """ - try: - results = self._rpc_balance(self._config.get('fiat_display_currency', '')) - return self.rest_dump(results) + results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + return self.rest_dump(results) - except RPCException as e: - logger.exception("API Error calling status table: %s", e) - return self.rest_error(f"{e}") + @safe_rpc + def _whitelist(self): + """ + Handler for /whitelist table. + + """ + results = self._rpc_whitelist() + return self.rest_dump(results) From 99875afcc034e6a7cdb96a2be5eb71917d3e9115 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Apr 2019 07:08:46 +0200 Subject: [PATCH 018/134] Add default argument --- freqtrade/rpc/api_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 06f4ad0b9..693b82bec 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -235,8 +235,7 @@ class ApiServer(RPC): :return: stats """ - timescale = request.args.get('timescale') - logger.info("LocalRPC - Daily Command Called") + timescale = request.args.get('timescale', 7) timescale = int(timescale) stats = self._rpc_daily_profit(timescale, From d2c2811249022671812e9a4f8754105cc9697782 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:45:15 +0200 Subject: [PATCH 019/134] Move rest-client to scripts --- {freqtrade/rpc => scripts}/rest_client.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {freqtrade/rpc => scripts}/rest_client.py (100%) diff --git a/freqtrade/rpc/rest_client.py b/scripts/rest_client.py similarity index 100% rename from freqtrade/rpc/rest_client.py rename to scripts/rest_client.py From 5ba189ffb41a7d887cc4d74b92e0ffb708f62194 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:55:38 +0200 Subject: [PATCH 020/134] Add more commands to rest client, fix bug in config handling --- scripts/rest_client.py | 46 ++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 51c7a88f5..bd1187a3e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -26,16 +26,28 @@ logger = logging.getLogger("ft_rest_client") COMMANDS_NO_ARGS = ["start", "stop", "stopbuy", - "reload_conf" + "reload_conf", ] -COMMANDS_ARGS = ["daily", - ] +INFO_COMMANDS = {"version": [], + "count": [], + "daily": ["timescale"], + "profit": [], + "status": [], + "balance": [] + } def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + + parser.add_argument("command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[] + ) + parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -58,7 +70,8 @@ def load_config(configfile): if file.is_file(): with file.open("r") as f: config = json.load(f) - return config + return config + return {} def call_authorized(url): @@ -74,6 +87,22 @@ def call_command_noargs(server_url, command): logger.info(r) +def call_info(server_url, command, command_args): + logger.info(f"Running command `{command}` with parameters `{command_args}` at {server_url}") + call = f"{server_url}/{command}?" + args = INFO_COMMANDS[command] + if len(args) < len(command_args): + logger.error(f"Command {command} does only support {len(args)} arguments.") + return + for idx, arg in enumerate(command_args): + + call += f"{args[idx]}={arg}" + logger.debug(call) + r = call_authorized(call) + + logger.info(r) + + def main(args): config = load_config(args["config"]) @@ -85,13 +114,8 @@ def main(args): if args["command"] in COMMANDS_NO_ARGS: call_command_noargs(server_url, args["command"]) - if args["command"] == "daily": - if str.isnumeric(argv[2]): - get_url = server_url + '/daily?timescale=' + argv[2] - d = get(get_url).json() - print(d) - else: - print("\nThe second argument to daily must be an integer, 1,2,3 etc") + if args["command"] in INFO_COMMANDS: + call_info(server_url, args["command"], args["command_arguments"]) if __name__ == "__main__": From a1043121fc0a760194ad434d79371368d835386d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2019 06:56:01 +0200 Subject: [PATCH 021/134] Add blacklist handler --- freqtrade/rpc/api_server.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 693b82bec..2c151daf3 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -116,11 +116,13 @@ class ApiServer(RPC): app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) + app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET']) # TODO: Implement the following # performance # forcebuy # forcesell - # blacklist + # whitelist param + # balacklist params # edge # help (?) @@ -264,7 +266,7 @@ class ApiServer(RPC): @safe_rpc def _status(self): """ - Handler for /status table. + Handler for /status. Returns the current status of the trades in json format """ @@ -274,7 +276,7 @@ class ApiServer(RPC): @safe_rpc def _balance(self): """ - Handler for /balance table. + Handler for /balance. Returns the current status of the trades in json format """ @@ -284,8 +286,15 @@ class ApiServer(RPC): @safe_rpc def _whitelist(self): """ - Handler for /whitelist table. - + Handler for /whitelist. """ results = self._rpc_whitelist() return self.rest_dump(results) + + @safe_rpc + def _blacklist(self): + """ + Handler for /blacklist. + """ + results = self._rpc_blacklist() + return self.rest_dump(results) From a132d6e141fccc257c0814fb6e24927e41f3a028 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Apr 2019 20:32:10 +0200 Subject: [PATCH 022/134] Refactor client into class --- scripts/rest_client.py | 95 +++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index bd1187a3e..4e576d3cd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,10 +10,10 @@ so it can be used as a standalone script. import argparse import json import logging -from sys import argv +from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path -from requests import get +import requests from requests.exceptions import ConnectionError logging.basicConfig( @@ -37,6 +37,63 @@ INFO_COMMANDS = {"version": [], } +class FtRestClient(): + + def __init__(self, serverurl): + self.serverurl = serverurl + + self.session = requests.Session() + + def call(self, method, apipath, params: dict = None, data=None, files=None): + + if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): + raise ValueError('invalid method <{0}>'.format(method)) + basepath = f"{self.serverurl}/{apipath}" + + hd = {"Accept": "application/json", + "Content-Type": "application/json" + } + + # Split url + schema, netloc, path, params, query, fragment = urlparse(basepath) + # URLEncode query string + query = urlencode(params) + # recombine url + url = urlunparse((schema, netloc, path, params, query, fragment)) + print(url) + try: + + req = requests.Request(method, url, headers=hd, data=data, + # auth=self.session.auth + ) + reqp = req.prepare() + return self.session.send(reqp).json() + # return requests.get(url).json() + except ConnectionError: + logger.warning("Connection error") + + def call_command_noargs(self, command): + logger.info(f"Running command `{command}` at {self.serverurl}") + r = self.call("GET", command) + logger.info(r) + + def call_info(self, command, command_args): + logger.info( + f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") + args = INFO_COMMANDS[command] + if len(args) < len(command_args): + logger.error(f"Command {command} does only support {len(args)} arguments.") + return + params = {} + for idx, arg in enumerate(command_args): + params[args[idx]] = arg + + logger.debug(params) + r = self.call("GET", command, params) + + logger.info(r) + + def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", @@ -74,48 +131,20 @@ def load_config(configfile): return {} -def call_authorized(url): - try: - return get(url).json() - except ConnectionError: - logger.warning("Connection error") - - -def call_command_noargs(server_url, command): - logger.info(f"Running command `{command}` at {server_url}") - r = call_authorized(f"{server_url}/{command}") - logger.info(r) - - -def call_info(server_url, command, command_args): - logger.info(f"Running command `{command}` with parameters `{command_args}` at {server_url}") - call = f"{server_url}/{command}?" - args = INFO_COMMANDS[command] - if len(args) < len(command_args): - logger.error(f"Command {command} does only support {len(args)} arguments.") - return - for idx, arg in enumerate(command_args): - - call += f"{args[idx]}={arg}" - logger.debug(call) - r = call_authorized(call) - - logger.info(r) - - def main(args): config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") server_url = f"http://{url}:{port}" + client = FtRestClient(server_url) # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - call_command_noargs(server_url, args["command"]) + client.call_command_noargs(args["command"]) if args["command"] in INFO_COMMANDS: - call_info(server_url, args["command"], args["command_arguments"]) + client.call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From b0ac98a7cd069ceffda15d018792bbff70acdaf7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:08:03 +0200 Subject: [PATCH 023/134] Clean up rest client --- scripts/rest_client.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4e576d3cd..e01fc086e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -40,8 +40,8 @@ INFO_COMMANDS = {"version": [], class FtRestClient(): def __init__(self, serverurl): - self.serverurl = serverurl + self.serverurl = serverurl self.session = requests.Session() def call(self, method, apipath, params: dict = None, data=None, files=None): @@ -62,19 +62,17 @@ class FtRestClient(): url = urlunparse((schema, netloc, path, params, query, fragment)) print(url) try: - - req = requests.Request(method, url, headers=hd, data=data, - # auth=self.session.auth - ) - reqp = req.prepare() - return self.session.send(reqp).json() - # return requests.get(url).json() + resp = self.session.request(method, url, headers=hd, data=data, + # auth=self.session.auth + ) + # return resp.text + return resp.json() except ConnectionError: logger.warning("Connection error") def call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") - r = self.call("GET", command) + r = self.call("POST", command) logger.info(r) def call_info(self, command, command_args): From ebebf94750f8fd56add5520ecd0e0ef2777bbebf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:10:07 +0200 Subject: [PATCH 024/134] Change commands to post --- freqtrade/rpc/api_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 2c151daf3..d520fd255 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -103,11 +103,11 @@ class ApiServer(RPC): """ # TODO: actions should not be GET... # Actions to control the bot - app.add_url_rule('/start', 'start', view_func=self._start, methods=['GET']) - app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['GET']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['GET']) + app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) + app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) + app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['GET']) + methods=['POST']) # Info commands app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) From d1fffab23580ff615d3bd3fbbe532c451c2ad69a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:10:23 +0200 Subject: [PATCH 025/134] Rename internal methods to _ --- scripts/rest_client.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index e01fc086e..6843bb31d 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -44,7 +44,7 @@ class FtRestClient(): self.serverurl = serverurl self.session = requests.Session() - def call(self, method, apipath, params: dict = None, data=None, files=None): + def _call(self, method, apipath, params: dict = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) @@ -70,12 +70,12 @@ class FtRestClient(): except ConnectionError: logger.warning("Connection error") - def call_command_noargs(self, command): + def _call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") - r = self.call("POST", command) + r = self._call("POST", command) logger.info(r) - def call_info(self, command, command_args): + def _call_info(self, command, command_args): logger.info( f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") args = INFO_COMMANDS[command] @@ -87,7 +87,7 @@ class FtRestClient(): params[args[idx]] = arg logger.debug(params) - r = self.call("GET", command, params) + r = self._call("GET", command, params) logger.info(r) @@ -139,10 +139,10 @@ def main(args): # Call commands without arguments if args["command"] in COMMANDS_NO_ARGS: - client.call_command_noargs(args["command"]) + client._call_command_noargs(args["command"]) if args["command"] in INFO_COMMANDS: - client.call_info(args["command"], args["command_arguments"]) + client._call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From 8f9b9d31e2f667cbaa0083b6982f01eae006d9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:12:03 +0200 Subject: [PATCH 026/134] Reorder arguments --- scripts/rest_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6843bb31d..fc3478046 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -97,12 +97,6 @@ def add_arguments(): parser.add_argument("command", help="Positional argument defining the command to execute.") - parser.add_argument("command_arguments", - help="Positional arguments for the parameters for [command]", - nargs="*", - default=[] - ) - parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -110,6 +104,13 @@ def add_arguments(): metavar='PATH', default='config.json' ) + + parser.add_argument("command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[] + ) + args = parser.parse_args() # if len(argv) == 1: # print('\nThis script accepts the following arguments') From 938d7275ba49b6cf718d6c8c5ff03958b13c4243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:27:20 +0200 Subject: [PATCH 027/134] implement some methods --- scripts/rest_client.py | 50 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index fc3478046..eb6fb97a9 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,6 +10,7 @@ so it can be used as a standalone script. import argparse import json import logging +import inspect from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path @@ -55,11 +56,11 @@ class FtRestClient(): } # Split url - schema, netloc, path, params, query, fragment = urlparse(basepath) + schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string query = urlencode(params) # recombine url - url = urlunparse((schema, netloc, path, params, query, fragment)) + url = urlunparse((schema, netloc, path, par, query, fragment)) print(url) try: resp = self.session.request(method, url, headers=hd, data=data, @@ -70,6 +71,12 @@ class FtRestClient(): except ConnectionError: logger.warning("Connection error") + def _get(self, apipath, params: dict = None): + return self._call("GET", apipath, params=params) + + def _post(self, apipath, params: dict = None, data: dict = None): + return self._call("POST", apipath, params=params, data=data) + def _call_command_noargs(self, command): logger.info(f"Running command `{command}` at {self.serverurl}") r = self._call("POST", command) @@ -91,6 +98,27 @@ class FtRestClient(): logger.info(r) + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + + def count(self): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("count") + + def daily(self, days=None): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("daily", params={"timescale": days} if days else None) + def add_arguments(): parser = argparse.ArgumentParser() @@ -138,12 +166,20 @@ def main(args): server_url = f"http://{url}:{port}" client = FtRestClient(server_url) - # Call commands without arguments - if args["command"] in COMMANDS_NO_ARGS: - client._call_command_noargs(args["command"]) + m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] + command = args["command"] + if command not in m: + logger.error(f"Command {command} not defined") + return - if args["command"] in INFO_COMMANDS: - client._call_info(args["command"], args["command_arguments"]) + print(getattr(client, command)(*args["command_arguments"])) + + # Call commands without arguments + # if args["command"] in COMMANDS_NO_ARGS: + # client._call_command_noargs(args["command"]) + + # if args["command"] in INFO_COMMANDS: + # client._call_info(args["command"], args["command_arguments"]) if __name__ == "__main__": From 122cf4c897268d8ba2ce7e8665f4f1a9989dda93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:11 +0200 Subject: [PATCH 028/134] Default add to None for blacklist rpc calls --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5b78c8356..048ebec63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -470,7 +470,7 @@ class RPC(object): } return res - def _rpc_blacklist(self, add: List[str]) -> Dict: + def _rpc_blacklist(self, add: List[str] = None) -> Dict: """ Returns the currently active blacklist""" if add: stake_currency = self._freqtrade.config.get('stake_currency') From 3efdd55fb8526a3c1a771bcc1c89928d191f86db Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:36 +0200 Subject: [PATCH 029/134] Support blacklist adding --- freqtrade/rpc/api_server.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d520fd255..1643bc535 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -115,14 +115,15 @@ class ApiServer(RPC): app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) - app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) + app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + methods=['GET', 'POST']) # TODO: Implement the following # performance # forcebuy # forcesell - # whitelist param - # balacklist params + # blacklist params # edge # help (?) @@ -296,5 +297,6 @@ class ApiServer(RPC): """ Handler for /blacklist. """ - results = self._rpc_blacklist() + add = request.json.get("blacklist", None) if request.method == 'POST' else None + results = self._rpc_blacklist(add) return self.rest_dump(results) From 0163edc868c3a8c2e9e4040387ea588415163d0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:55:52 +0200 Subject: [PATCH 030/134] rest-client more methods --- scripts/rest_client.py | 123 +++++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index eb6fb97a9..1958c1a07 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -11,6 +11,7 @@ import argparse import json import logging import inspect +from typing import List from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path @@ -24,32 +25,18 @@ logging.basicConfig( logger = logging.getLogger("ft_rest_client") -COMMANDS_NO_ARGS = ["start", - "stop", - "stopbuy", - "reload_conf", - ] -INFO_COMMANDS = {"version": [], - "count": [], - "daily": ["timescale"], - "profit": [], - "status": [], - "balance": [] - } - - class FtRestClient(): def __init__(self, serverurl): - self.serverurl = serverurl - self.session = requests.Session() + self._serverurl = serverurl + self._session = requests.Session() def _call(self, method, apipath, params: dict = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) - basepath = f"{self.serverurl}/{apipath}" + basepath = f"{self._serverurl}/{apipath}" hd = {"Accept": "application/json", "Content-Type": "application/json" @@ -58,14 +45,14 @@ class FtRestClient(): # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string - query = urlencode(params) + query = urlencode(params) if params else None # recombine url url = urlunparse((schema, netloc, path, par, query, fragment)) - print(url) + try: - resp = self.session.request(method, url, headers=hd, data=data, - # auth=self.session.auth - ) + resp = self._session.request(method, url, headers=hd, data=json.dumps(data), + # auth=self.session.auth + ) # return resp.text return resp.json() except ConnectionError: @@ -77,33 +64,12 @@ class FtRestClient(): def _post(self, apipath, params: dict = None, data: dict = None): return self._call("POST", apipath, params=params, data=data) - def _call_command_noargs(self, command): - logger.info(f"Running command `{command}` at {self.serverurl}") - r = self._call("POST", command) - logger.info(r) - - def _call_info(self, command, command_args): - logger.info( - f"Running command `{command}` with parameters `{command_args}` at {self.serverurl}") - args = INFO_COMMANDS[command] - if len(args) < len(command_args): - logger.error(f"Command {command} does only support {len(args)} arguments.") - return - params = {} - for idx, arg in enumerate(command_args): - params[args[idx]] = arg - - logger.debug(params) - r = self._call("GET", command, params) - - logger.info(r) - - def version(self): + def balance(self): """ - Returns the version of the bot - :returns: json object containing the version + get the account balance + :returns: json object """ - return self._get("version") + return self._get("balance") def count(self): """ @@ -119,12 +85,58 @@ class FtRestClient(): """ return self._get("daily", params={"timescale": days} if days else None) + def profit(self): + """ + Returns the profit summary + :returns: json object + """ + return self._get("profit") + + def status(self): + """ + Get the status of open trades + :returns: json object + """ + return self._get("status") + + def whitelist(self): + """ + Show the current whitelist + :returns: json object + """ + return self._get("whitelist") + + def blacklist(self, *args): + """ + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: json object + """ + if not args: + return self._get("blacklist") + else: + return self._post("blacklist", data={"blacklist": args}) + + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Positional argument defining the command to execute.") + parser.add_argument('--show', + help='Show possible methods with this client', + dest='show', + action='store_true', + default=False + ) + parser.add_argument('-c', '--config', help='Specify configuration file (default: %(default)s). ', dest='config', @@ -160,6 +172,16 @@ def load_config(configfile): def main(args): + if args.get("show"): + # Print dynamic help for the different commands + client = FtRestClient(None) + print("Possible commands:") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + print(f"{x} {getattr(client, x).__doc__}") + + return + config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") @@ -174,13 +196,6 @@ def main(args): print(getattr(client, command)(*args["command_arguments"])) - # Call commands without arguments - # if args["command"] in COMMANDS_NO_ARGS: - # client._call_command_noargs(args["command"]) - - # if args["command"] in INFO_COMMANDS: - # client._call_info(args["command"], args["command_arguments"]) - if __name__ == "__main__": args = add_arguments() From 393e4ac90e0c435a37be62060c144835dae0b853 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 09:59:08 +0200 Subject: [PATCH 031/134] Sort methods --- freqtrade/rpc/api_server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 1643bc535..009ae7d68 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -109,16 +109,18 @@ class ApiServer(RPC): app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, methods=['POST']) # Info commands - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, - methods=['GET']) + app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + + # Combined actions and infos app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) + app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) # TODO: Implement the following # performance # forcebuy From b1964851c9d9bc7153abf6b93a774bf2a4873fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:03:54 +0200 Subject: [PATCH 032/134] Add performance handlers --- freqtrade/rpc/api_server.py | 18 ++++++++++++++++-- scripts/rest_client.py | 9 ++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 009ae7d68..f63c68bf1 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -113,6 +113,8 @@ class ApiServer(RPC): app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) + app.add_url_rule('/performance', 'performance', view_func=self._performance, + methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) @@ -122,10 +124,8 @@ class ApiServer(RPC): app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) # TODO: Implement the following - # performance # forcebuy # forcesell - # blacklist params # edge # help (?) @@ -266,6 +266,20 @@ class ApiServer(RPC): return self.rest_dump(stats) + @safe_rpc + def _performance(self): + """ + Handler for /performance. + + Returns a cumulative performance statistics + :return: stats + """ + logger.info("LocalRPC - performance Command Called") + + stats = self._rpc_performance() + + return self.rest_dump(stats) + @safe_rpc def _status(self): """ diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 1958c1a07..7dbed9bc5 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -45,7 +45,7 @@ class FtRestClient(): # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string - query = urlencode(params) if params else None + query = urlencode(params) if params else "" # recombine url url = urlunparse((schema, netloc, path, par, query, fragment)) @@ -92,6 +92,13 @@ class FtRestClient(): """ return self._get("profit") + def performance(self): + """ + Returns the performance of the different coins + :returns: json object + """ + return self._get("performance") + def status(self): """ Get the status of open trades From ea8b8eec1ca178ca151796f19aaef7c48bae7624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:06:46 +0200 Subject: [PATCH 033/134] Add edge handler --- freqtrade/rpc/api_server.py | 12 +++++++++++- scripts/rest_client.py | 21 ++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f63c68bf1..bde28be73 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -112,6 +112,7 @@ class ApiServer(RPC): app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) app.add_url_rule('/performance', 'performance', view_func=self._performance, methods=['GET']) @@ -126,7 +127,6 @@ class ApiServer(RPC): # TODO: Implement the following # forcebuy # forcesell - # edge # help (?) def run(self): @@ -250,6 +250,16 @@ class ApiServer(RPC): return self.rest_dump(stats) + @safe_rpc + def _edge(self): + """ + Returns information related to Edge. + :return: edge stats + """ + stats = self._rpc_edge() + + return self.rest_dump(stats) + @safe_rpc def _profit(self): """ diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 7dbed9bc5..efca8adfa 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -85,6 +85,13 @@ class FtRestClient(): """ return self._get("daily", params={"timescale": days} if days else None) + def edge(self): + """ + Returns information about edge + :returns: json object + """ + return self._get("edge") + def profit(self): """ Returns the profit summary @@ -106,6 +113,13 @@ class FtRestClient(): """ return self._get("status") + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + def whitelist(self): """ Show the current whitelist @@ -124,13 +138,6 @@ class FtRestClient(): else: return self._post("blacklist", data={"blacklist": args}) - def version(self): - """ - Returns the version of the bot - :returns: json object containing the version - """ - return self._get("version") - def add_arguments(): parser = argparse.ArgumentParser() From cb271f51d1f288cebaacb45572b6948b8f763d22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:10:01 +0200 Subject: [PATCH 034/134] Add client actions for actions --- scripts/rest_client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index efca8adfa..c8f5823a1 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -64,6 +64,35 @@ class FtRestClient(): def _post(self, apipath, params: dict = None, data: dict = None): return self._call("POST", apipath, params=params, data=data) + def start(self): + """ + Start the bot if it's in stopped state. + :returns: json object + """ + return self._post("start") + + def stop(self): + """ + Stop the bot. Use start to restart + :returns: json object + """ + return self._post("stop") + + def stopbuy(self): + """ + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + """ + return self._post("stopbuy") + + def reload_conf(self): + """ + Reload configuration + :returns: json object + """ + return self._post("reload_conf") + def balance(self): """ get the account balance From bc4342b2d0119ba5b9baa97ffddabe1e5669557a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 10:12:39 +0200 Subject: [PATCH 035/134] small cleanup --- freqtrade/rpc/api_server.py | 7 +++---- scripts/rest_client.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index bde28be73..e158df0be 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -101,7 +101,6 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ - # TODO: actions should not be GET... # Actions to control the bot app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) @@ -114,7 +113,7 @@ class ApiServer(RPC): app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/performance', 'performance', view_func=self._performance, + app.add_url_rule('/performance', 'performance', view_func=self._performance, methods=['GET']) app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) @@ -141,8 +140,8 @@ class ApiServer(RPC): logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) if not IPv4Address(rest_ip).is_loopback: - logger.info("SECURITY WARNING - Local Rest Server listening to external connections") - logger.info("SECURITY WARNING - This is insecure please set to your loopback," + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json") # Run the Server diff --git a/scripts/rest_client.py b/scripts/rest_client.py index c8f5823a1..4830d43b8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -95,7 +95,7 @@ class FtRestClient(): def balance(self): """ - get the account balance + Get the account balance :returns: json object """ return self._get("balance") From 6e4b159611128bce19111b0141ff8d5b6da8f61a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2019 12:50:13 +0200 Subject: [PATCH 036/134] Add forcebuy and forcesell --- freqtrade/rpc/api_server.py | 25 +++++++++++++++++++++++-- scripts/rest_client.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e158df0be..d47def4a6 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -123,9 +123,10 @@ class ApiServer(RPC): methods=['GET', 'POST']) app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) + app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) + app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) + # TODO: Implement the following - # forcebuy - # forcesell # help (?) def run(self): @@ -325,3 +326,23 @@ class ApiServer(RPC): add = request.json.get("blacklist", None) if request.method == 'POST' else None results = self._rpc_blacklist(add) return self.rest_dump(results) + + @safe_rpc + def _forcebuy(self): + """ + Handler for /forcebuy. + """ + asset = request.json.get("pair") + price = request.json.get("price", None) + trade = self._rpc_forcebuy(asset, price) + # TODO: Returns a trade, we need to jsonify that. + return self.rest_dump(trade) + + @safe_rpc + def _forcesell(self): + """ + Handler for /forcesell. + """ + tradeid = request.json.get("tradeid") + results = self._rpc_forcesell(tradeid) + return self.rest_dump(results) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4830d43b8..81c4b66cc 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -167,6 +167,27 @@ class FtRestClient(): else: return self._post("blacklist", data={"blacklist": args}) + def forcebuy(self, pair, price=None): + """ + Buy an asset + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :returns: json object of the trade + """ + data = {"pair": pair, + "price": price + } + return self._post("forcebuy", data=data) + + def forcesell(self, tradeid): + """ + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + """ + + return self._post("forcesell", data={"tradeid": tradeid}) + def add_arguments(): parser = argparse.ArgumentParser() From 0ac434da78613714dc27300fee7f5750ae872cff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 May 2019 07:12:37 +0200 Subject: [PATCH 037/134] Add forcebuy jsonification --- freqtrade/rpc/api_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d47def4a6..be2f02663 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -335,8 +335,7 @@ class ApiServer(RPC): asset = request.json.get("pair") price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) - # TODO: Returns a trade, we need to jsonify that. - return self.rest_dump(trade) + return self.rest_dump(trade.to_json()) @safe_rpc def _forcesell(self): From e0486ea68eb5e2243dbc8ae7125013e4827935af Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 May 2019 07:07:14 +0200 Subject: [PATCH 038/134] Make app a instance object --- freqtrade/rpc/api_server.py | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be2f02663..be6b20ecb 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -26,10 +26,6 @@ class ArrowJSONEncoder(JSONEncoder): return JSONEncoder.default(self, obj) -app = Flask(__name__) -app.json_encoder = ArrowJSONEncoder - - class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it @@ -58,6 +54,9 @@ class ApiServer(RPC): super().__init__(freqtrade) self._config = freqtrade.config + self.app = Flask(__name__) + + self.app.json_encoder = ArrowJSONEncoder # Register application handling self.register_rest_other() @@ -90,8 +89,8 @@ class ApiServer(RPC): Registers flask app URLs that are not calls to functionality in rpc.rpc. :return: """ - app.register_error_handler(404, self.page_not_found) - app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + self.app.register_error_handler(404, self.page_not_found) + self.app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) def register_rest_rpc_urls(self): """ @@ -102,29 +101,30 @@ class ApiServer(RPC): :return: """ # Actions to control the bot - app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) - app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) - app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['POST']) + self.app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) + self.app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) + self.app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) + self.app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, + methods=['POST']) # Info commands - app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) - app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) - app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) - app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - app.add_url_rule('/performance', 'performance', view_func=self._performance, - methods=['GET']) - app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) + self.app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) + self.app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) + self.app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) + self.app.add_url_rule('/performance', 'performance', view_func=self._performance, + methods=['GET']) + self.app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) + self.app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) # Combined actions and infos - app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, - methods=['GET', 'POST']) - app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, - methods=['GET']) - app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) - app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) + self.app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + methods=['GET', 'POST']) + self.app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) + self.app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) + self.app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, + methods=['POST']) # TODO: Implement the following # help (?) @@ -143,12 +143,12 @@ class ApiServer(RPC): if not IPv4Address(rest_ip).is_loopback: logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") + "e.g 127.0.0.1 in config.json") # Run the Server logger.info('Starting Local Rest Server') try: - app.run(host=rest_ip, port=rest_port) + self.app.run(host=rest_ip, port=rest_port) except Exception: logger.exception("Api server failed to start, exception message is:") logger.info('Starting Local Rest Server_end') From b1a14401c2ee83cd3f61ecc6c3a99dac6cd5e9ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 May 2019 07:07:38 +0200 Subject: [PATCH 039/134] Add some initial tests for apiserver --- freqtrade/tests/rpc/test_rpc_apiserver.py | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 14c35a38e..fae278028 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -4,11 +4,37 @@ Unit test file for rpc/api_server.py from unittest.mock import MagicMock +import pytest + from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver +@pytest.fixture +def client(default_conf, mocker): + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + yield apiserver.app.test_client() + # Cleanup ... ? + + +def response_success_assert(response): + assert response.status_code == 200 + assert response.content_type == "application/json" + + +def test_start(client): + rc = client.post("/start") + response_success_assert(rc) + assert rc.json == {'status': 'already running'} + + +def test_stop(client): + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'stopping trader ...'} + + def test__init__(default_conf, mocker): """ Test __init__() method From 6ea08958032d65efec043f5b1f395bb549977ad1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 08:55:10 +0200 Subject: [PATCH 040/134] Fix docstrings --- freqtrade/rpc/api_server.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be6b20ecb..18c68e797 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -130,9 +130,7 @@ class ApiServer(RPC): # help (?) def run(self): - """ Method that runs flask app in its own thread forever """ - - """ + """ Method that runs flask app in its own thread forever. Section to handle configuration and running of the Rest server also to check and warn if not bound to a loopback, warn on security risk. """ @@ -153,11 +151,6 @@ class ApiServer(RPC): logger.exception("Api server failed to start, exception message is:") logger.info('Starting Local Rest Server_end') - """ - Define the application methods here, called by app.add_url_rule - each Telegram command should have a like local substitute - """ - def page_not_found(self, error): """ Return "404 not found", 404. From 70a3c2c648f5aa3c0c09ea650241aed7dea63c84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 08:55:21 +0200 Subject: [PATCH 041/134] Actions - Add tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 75 ++++++++++++----------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index fae278028..8062171c5 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -12,9 +12,10 @@ from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver @pytest.fixture -def client(default_conf, mocker): - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) - yield apiserver.app.test_client() +def botclient(default_conf, mocker): + ftbot = get_patched_freqtradebot(mocker, default_conf) + apiserver = ApiServer(ftbot) + yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -23,19 +24,32 @@ def response_success_assert(response): assert response.content_type == "application/json" -def test_start(client): +def test_api_stop_workflow(botclient): + ftbot, client = botclient + assert ftbot.state == State.RUNNING + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'stopping trader ...'} + assert ftbot.state == State.STOPPED + + # Stop bot again + rc = client.post("/stop") + response_success_assert(rc) + assert rc.json == {'status': 'already stopped'} + + # Start bot + rc = client.post("/start") + response_success_assert(rc) + assert rc.json == {'status': 'starting trader ...'} + assert ftbot.state == State.RUNNING + + # Call start again rc = client.post("/start") response_success_assert(rc) assert rc.json == {'status': 'already running'} -def test_stop(client): - rc = client.post("/stop") - response_success_assert(rc) - assert rc.json == {'status': 'stopping trader ...'} - - -def test__init__(default_conf, mocker): +def test_api__init__(default_conf, mocker): """ Test __init__() method """ @@ -46,33 +60,20 @@ def test__init__(default_conf, mocker): assert apiserver._config == default_conf -def test_start_endpoint(default_conf, mocker): - """Test /start endpoint""" - patch_apiserver(mocker) - bot = get_patched_freqtradebot(mocker, default_conf) - apiserver = ApiServer(bot) +def test_api_reloadconf(botclient): + ftbot, client = botclient - bot.state = State.STOPPED - assert bot.state == State.STOPPED - result = apiserver.start() - assert result == '{"status": "starting trader ..."}' - assert bot.state == State.RUNNING - - result = apiserver.start() - assert result == '{"status": "already running"}' + rc = client.post("/reload_conf") + response_success_assert(rc) + assert rc.json == {'status': 'reloading config ...'} + assert ftbot.state == State.RELOAD_CONF -def test_stop_endpoint(default_conf, mocker): - """Test /stop endpoint""" - patch_apiserver(mocker) - bot = get_patched_freqtradebot(mocker, default_conf) - apiserver = ApiServer(bot) +def test_api_stopbuy(botclient): + ftbot, client = botclient + assert ftbot.config['max_open_trades'] != 0 - bot.state = State.RUNNING - assert bot.state == State.RUNNING - result = apiserver.stop() - assert result == '{"status": "stopping trader ..."}' - assert bot.state == State.STOPPED - - result = apiserver.stop() - assert result == '{"status": "already stopped"}' + rc = client.post("/stopbuy") + response_success_assert(rc) + assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + assert ftbot.config['max_open_trades'] == 0 From 6b426e78f60745fec291ecd192cb00837bee3374 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:10:54 +0200 Subject: [PATCH 042/134] Tests for balance --- freqtrade/tests/conftest.py | 36 ++++++++++++++++++++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 37 +++++++++++++++++++++++ freqtrade/tests/rpc/test_rpc_telegram.py | 36 ++-------------------- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 98563a374..a4eae4300 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -970,3 +970,39 @@ def edge_conf(default_conf): } return default_conf + + +@pytest.fixture +def rpc_balance(): + return { + 'BTC': { + 'total': 12.0, + 'free': 12.0, + 'used': 0.0 + }, + 'ETH': { + 'total': 0.0, + 'free': 0.0, + 'used': 0.0 + }, + 'USDT': { + 'total': 10000.0, + 'free': 10000.0, + 'used': 0.0 + }, + 'LTC': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + 'XRP': { + 'total': 1.0, + 'free': 1.0, + 'used': 0.0 + }, + 'EUR': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + } diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 8062171c5..7e99a9510 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -77,3 +77,40 @@ def test_api_stopbuy(botclient): response_success_assert(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 + + +def test_api_balance(botclient, mocker, rpc_balance): + ftbot, client = botclient + + def mock_ticker(symbol, refresh): + if symbol == 'BTC/USDT': + return { + 'bid': 10000.00, + 'ask': 10000.00, + 'last': 10000.00, + } + elif symbol == 'XRP/BTC': + return { + 'bid': 0.00001, + 'ask': 0.00001, + 'last': 0.00001, + } + return { + 'bid': 0.1, + 'ask': 0.1, + 'last': 0.1, + } + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + + rc = client.get("/balance") + response_success_assert(rc) + assert "currencies" in rc.json + assert len(rc.json["currencies"]) == 5 + assert rc.json['currencies'][0] == { + 'currency': 'BTC', + 'available': 12.0, + 'balance': 12.0, + 'pending': 0.0, + 'est_btc': 12.0, + } diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 69e3006cd..b8e57d092 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -496,39 +496,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_telegram_balance_handle(default_conf, update, mocker) -> None: - mock_balance = { - 'BTC': { - 'total': 12.0, - 'free': 12.0, - 'used': 0.0 - }, - 'ETH': { - 'total': 0.0, - 'free': 0.0, - 'used': 0.0 - }, - 'USDT': { - 'total': 10000.0, - 'free': 10000.0, - 'used': 0.0 - }, - 'LTC': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - }, - 'XRP': { - 'total': 1.0, - 'free': 1.0, - 'used': 0.0 - }, - 'EUR': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - } - } +def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: def mock_ticker(symbol, refresh): if symbol == 'BTC/USDT': @@ -549,7 +517,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) msg_mock = MagicMock() From 88dd18e045bc0bae78c3b7203674842d4485c484 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:42:30 +0200 Subject: [PATCH 043/134] Move patch_signal to conftest --- freqtrade/tests/conftest.py | 13 +++++++++++-- freqtrade/tests/rpc/test_rpc.py | 3 +-- freqtrade/tests/rpc/test_rpc_telegram.py | 3 +-- freqtrade/tests/test_freqtradebot.py | 11 +---------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index a4eae4300..c9b98aacd 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -10,7 +10,7 @@ import arrow import pytest from telegram import Chat, Message, Update -from freqtrade import constants +from freqtrade import constants, persistence from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -96,7 +96,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + persistence.init(config) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -112,6 +112,15 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: + """ + :param mocker: mocker to patch IStrategy class + :param value: which value IStrategy.get_signal() must return + :return: None + """ + freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.exchange.refresh_latest_ohlcv = lambda p: None + @pytest.fixture(autouse=True) def patch_coinmarketcap(mocker) -> None: """ diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 6ce543f3d..f005041d9 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -14,8 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.conftest import patch_exchange -from freqtrade.tests.test_freqtradebot import patch_get_signal +from freqtrade.tests.conftest import patch_exchange, patch_get_signal # Functions for recurrent object patching diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b8e57d092..46ef15f56 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -22,8 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, - patch_exchange) -from freqtrade.tests.test_freqtradebot import patch_get_signal + patch_exchange, patch_get_signal) class DummyCls(Telegram): diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 67b05ac3e..946a9c819 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -19,7 +19,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, +from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, patch_get_signal, patch_exchange, patch_wallet) from freqtrade.worker import Worker @@ -59,15 +59,6 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: - """ - :param mocker: mocker to patch IStrategy class - :param value: which value IStrategy.get_signal() must return - :return: None - """ - freqtrade.strategy.get_signal = lambda e, s, t: value - freqtrade.exchange.refresh_latest_ohlcv = lambda p: None - def patch_RPCManager(mocker) -> MagicMock: """ From 3c468701093e67f64b663dedbc36f56ac4531e64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 09:44:39 +0200 Subject: [PATCH 044/134] Test /count for api-server --- freqtrade/tests/rpc/test_rpc_apiserver.py | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 7e99a9510..3590b0dc8 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -2,18 +2,23 @@ Unit test file for rpc/api_server.py """ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest +from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver +from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver, patch_get_signal @pytest.fixture def botclient(default_conf, mocker): + default_conf.update({"api_server":{"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -79,6 +84,14 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 +def test_api_version(botclient): + ftbot, client = botclient + + rc = client.get("/version") + response_success_assert(rc) + assert rc.json == {"version": __version__} + + def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient @@ -114,3 +127,27 @@ def test_api_balance(botclient, mocker, rpc_balance): 'pending': 0.0, 'est_btc': 12.0, } + + +def test_api_count(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/count") + response_success_assert(rc) + + assert rc.json["current"] == 0 + assert rc.json["max"] == 1.0 + + # Create some test data + ftbot.create_trade() + rc = client.get("/count") + response_success_assert(rc) + assert rc.json["current"] == 1.0 + assert rc.json["max"] == 1.0 From 03dc6d92aeb1371ff80562971a82700c386fac4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:10:41 +0200 Subject: [PATCH 045/134] Remove hello() --- freqtrade/rpc/api_server.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 18c68e797..5b7da902d 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,7 +90,6 @@ class ApiServer(RPC): :return: """ self.app.register_error_handler(404, self.page_not_found) - self.app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) def register_rest_rpc_urls(self): """ @@ -161,24 +160,6 @@ class ApiServer(RPC): 'code': 404 }), 404 - def hello(self): - """ - None critical but helpful default index page. - - That lists URLs added to the flask server. - This may be deprecated at any time. - :return: index.html - """ - rest_cmds = ('Commands implemented:
' - 'Show 7 days of stats
' - 'Stop the Trade thread
' - 'Start the Traded thread
' - 'Show profit summary
' - 'Show status table - Open trades
' - ' 404 page does not exist
' - ) - return rest_cmds - def _start(self): """ Handler for /start. From 557f849519ef144693135f2f5f51404c28f3bd7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:18:11 +0200 Subject: [PATCH 046/134] Improve 404 handling --- freqtrade/rpc/api_server.py | 2 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5b7da902d..573f89143 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -156,7 +156,7 @@ class ApiServer(RPC): """ return self.rest_dump({ 'status': 'error', - 'reason': '''There's no API call for %s''' % request.base_url, + 'reason': f"There's no API call for {request.base_url}.", 'code': 404 }), 404 diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 3590b0dc8..52a456d68 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -29,6 +29,18 @@ def response_success_assert(response): assert response.content_type == "application/json" +def test_api_not_found(botclient): + ftbot, client = botclient + + rc = client.post("/invalid_url") + assert rc.status_code == 404 + assert rc.content_type == "application/json" + assert rc.json == {'status': 'error', + 'reason': "There's no API call for http://localhost/invalid_url.", + 'code': 404 + } + + def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING From a146c5bf7820402470440620eae26f4f145743e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:31:48 +0200 Subject: [PATCH 047/134] Improve jsonification --- freqtrade/rpc/api_server.py | 5 +++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 573f89143..4ddda307a 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,5 +1,6 @@ import logging import threading +from datetime import datetime, date from ipaddress import IPv4Address from typing import Dict @@ -18,6 +19,10 @@ class ArrowJSONEncoder(JSONEncoder): try: if isinstance(obj, Arrow): return obj.for_json() + elif isinstance(obj, date): + return obj.strftime("%Y-%m-%d") + elif isinstance(obj, datetime): + return obj.strftime("%Y-%m-%d %H:%M:%S") iterable = iter(obj) except TypeError: pass diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 52a456d68..cefbb9acd 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -2,6 +2,7 @@ Unit test file for rpc/api_server.py """ +from datetime import datetime from unittest.mock import MagicMock, PropertyMock import pytest @@ -9,12 +10,13 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import get_patched_freqtradebot, patch_apiserver, patch_get_signal +from freqtrade.tests.conftest import (get_patched_freqtradebot, + patch_apiserver, patch_get_signal) @pytest.fixture def botclient(default_conf, mocker): - default_conf.update({"api_server":{"enabled": True, + default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) @@ -163,3 +165,19 @@ def test_api_count(botclient, mocker, ticker, fee, markets): response_success_assert(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 + + +def test_api_daily(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/daily") + response_success_assert(rc) + assert len(rc.json) == 7 + assert rc.json[0][0] == str(datetime.utcnow().date()) From a7329e5cc945efb005e02685d66d7b8b65ed56cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 13:40:30 +0200 Subject: [PATCH 048/134] Test api-server start from manager --- freqtrade/tests/conftest.py | 9 ------- freqtrade/tests/rpc/test_rpc_apiserver.py | 7 +++--- freqtrade/tests/rpc/test_rpc_manager.py | 29 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index c9b98aacd..bb8bd9c95 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -143,15 +143,6 @@ def patch_coinmarketcap(mocker) -> None: ) -def patch_apiserver(mocker) -> None: - mocker.patch.multiple( - 'freqtrade.rpc.api_server.ApiServer', - run=MagicMock(), - register_rest_other=MagicMock(), - register_rest_rpc_urls=MagicMock(), - ) - - @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index cefbb9acd..34fe558f8 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -10,15 +10,14 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import (get_patched_freqtradebot, - patch_apiserver, patch_get_signal) +from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) @pytest.fixture def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, - "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) ftbot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 15d9c20c6..91fd2297f 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -135,3 +135,32 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] + + +def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + default_conf['telegram']['enabled'] = False + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert not log_has('Enabling rpc.api_server', caplog.record_tuples) + assert rpc_manager.registered_modules == [] + assert run_mock.call_count == 0 + + +def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + + default_conf["telegram"]["enabled"] = False + default_conf["api_server"] = {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"} + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert log_has('Enabling rpc.api_server', caplog.record_tuples) + assert len(rpc_manager.registered_modules) == 1 + assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] + assert run_mock.call_count == 1 From b9435e3ceac2342e8aa853c7c9f1247f66a8790a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 May 2019 14:05:25 +0200 Subject: [PATCH 049/134] Add more tests --- freqtrade/tests/conftest.py | 1 + freqtrade/tests/rpc/test_rpc_apiserver.py | 220 +++++++++++++++++++--- freqtrade/tests/test_freqtradebot.py | 10 +- 3 files changed, 203 insertions(+), 28 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index bb8bd9c95..59989d604 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -121,6 +121,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.strategy.get_signal = lambda e, s, t: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None + @pytest.fixture(autouse=True) def patch_coinmarketcap(mocker) -> None: """ diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 34fe558f8..95ad7dbc3 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -3,7 +3,7 @@ Unit test file for rpc/api_server.py """ from datetime import datetime -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -11,6 +11,7 @@ from freqtrade.__init__ import __version__ from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) +from freqtrade.persistence import Trade @pytest.fixture @@ -25,8 +26,8 @@ def botclient(default_conf, mocker): # Cleanup ... ? -def response_success_assert(response): - assert response.status_code == 200 +def assert_response(response, expected_code=200): + assert response.status_code == expected_code assert response.content_type == "application/json" @@ -34,8 +35,7 @@ def test_api_not_found(botclient): ftbot, client = botclient rc = client.post("/invalid_url") - assert rc.status_code == 404 - assert rc.content_type == "application/json" + assert_response(rc, 404) assert rc.json == {'status': 'error', 'reason': "There's no API call for http://localhost/invalid_url.", 'code': 404 @@ -46,24 +46,24 @@ def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING rc = client.post("/stop") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again rc = client.post("/stop") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot rc = client.post("/start") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again rc = client.post("/start") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'already running'} @@ -82,7 +82,7 @@ def test_api_reloadconf(botclient): ftbot, client = botclient rc = client.post("/reload_conf") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -92,19 +92,11 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] != 0 rc = client.post("/stopbuy") - response_success_assert(rc) + assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 -def test_api_version(botclient): - ftbot, client = botclient - - rc = client.get("/version") - response_success_assert(rc) - assert rc.json == {"version": __version__} - - def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient @@ -130,7 +122,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) rc = client.get("/balance") - response_success_assert(rc) + assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 assert rc.json['currencies'][0] == { @@ -153,7 +145,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) rc = client.get("/count") - response_success_assert(rc) + assert_response(rc) assert rc.json["current"] == 0 assert rc.json["max"] == 1.0 @@ -161,7 +153,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() rc = client.get("/count") - response_success_assert(rc) + assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -177,6 +169,188 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) rc = client.get("/daily") - response_success_assert(rc) + assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) + + +def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client.get("/edge") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} + + +def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client.get("/profit") + assert_response(rc, 502) + assert len(rc.json) == 1 + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + ftbot.create_trade() + trade = Trade.query.first() + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + rc = client.get("/profit") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + rc = client.get("/profit") + assert_response(rc) + assert rc.json == {'avg_duration': '0:00:00', + 'best_pair': 'ETH/BTC', + 'best_rate': 6.2, + 'first_trade_date': 'just now', + 'latest_trade_date': 'just now', + 'profit_all_coin': 6.217e-05, + 'profit_all_fiat': 0, + 'profit_all_percent': 6.2, + 'profit_closed_coin': 6.217e-05, + 'profit_closed_fiat': 0, + 'profit_closed_percent': 6.2, + 'trade_count': 1 + } + + +def test_api_performance(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + + trade = Trade( + pair='LTC/ETH', + amount=1, + exchange='binance', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + + trade = Trade( + pair='XRP/ETH', + amount=5, + stake_amount=1, + exchange='binance', + open_rate=0.412, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.391 + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + Trade.session.flush() + + rc = client.get("/performance") + assert_response(rc) + assert len(rc.json) == 2 + assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] + + +def test_api_status(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client.get("/status") + assert_response(rc, 502) + assert rc.json == {'error': 'Error querying _status: no active trade'} + + ftbot.create_trade() + rc = client.get("/status") + assert_response(rc) + assert len(rc.json) == 1 + assert rc.json == [{'amount': 90.99181074, + 'base_currency': 'BTC', + 'close_date': None, + 'close_date_hum': None, + 'close_profit': None, + 'close_rate': None, + 'current_profit': -0.59, + 'current_rate': 1.098e-05, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_order': '(limit buy rem=0.00000000)', + 'open_rate': 1.099e-05, + 'pair': 'ETH/BTC', + 'stake_amount': 0.001, + 'stop_loss': 0.0, + 'stop_loss_pct': None, + 'trade_id': 1}] + + +def test_api_version(botclient): + ftbot, client = botclient + + rc = client.get("/version") + assert_response(rc) + assert rc.json == {"version": __version__} + + +def test_api_blacklist(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + + rc = client.get("/blacklist") + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], + "length": 2, + "method": "StaticPairList"} + + # Add ETH/BTC to blacklist + rc = client.post("/blacklist", data='{"blacklist": ["ETH/BTC"]}', + content_type='application/json') + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], + "length": 3, + "method": "StaticPairList"} + + +def test_api_whitelist(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + + rc = client.get("/whitelist") + assert_response(rc) + assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], + "length": 4, + "method": "StaticPairList"} + diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 946a9c819..4407859bf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -11,16 +11,17 @@ import arrow import pytest import requests -from freqtrade import (DependencyException, OperationalException, - TemporaryError, InvalidOrderException, constants) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError, constants) from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType -from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, patch_get_signal, - patch_exchange, patch_wallet) +from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, + patch_exchange, patch_get_signal, + patch_wallet) from freqtrade.worker import Worker @@ -59,7 +60,6 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) - def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests From 39afe4c7bd5cd92d14e728051f4b27409c9fda81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2019 07:07:51 +0200 Subject: [PATCH 050/134] Test flask app .run() --- freqtrade/rpc/api_server.py | 2 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 49 +++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4ddda307a..c2d83d1fb 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -141,7 +141,7 @@ class ApiServer(RPC): rest_ip = self._config['api_server']['listen_ip_address'] rest_port = self._config['api_server']['listen_port'] - logger.info('Starting HTTP Server at {}:{}'.format(rest_ip, rest_port)) + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') if not IPv4Address(rest_ip).is_loopback: logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") logger.warning("SECURITY WARNING - This is insecure please set to your loopback," diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 95ad7dbc3..41d682d2d 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -8,10 +8,11 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from freqtrade.__init__ import __version__ +from freqtrade.persistence import Trade from freqtrade.rpc.api_server import ApiServer from freqtrade.state import State -from freqtrade.tests.conftest import (get_patched_freqtradebot, patch_get_signal) -from freqtrade.persistence import Trade +from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, + patch_get_signal) @pytest.fixture @@ -78,6 +79,49 @@ def test_api__init__(default_conf, mocker): assert apiserver._config == default_conf +def test_api_run(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + + # Monkey patch flask app + run_mock = MagicMock() + apiserver.app = MagicMock() + apiserver.app.run = run_mock + + assert apiserver._config == default_conf + apiserver.run() + assert run_mock.call_count == 1 + assert run_mock.call_args_list[0][1]["host"] == "127.0.0.1" + assert run_mock.call_args_list[0][1]["port"] == "8080" + + assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) + assert log_has("Starting Local Rest Server", caplog.record_tuples) + + # Test binding to public + caplog.clear() + run_mock.reset_mock() + apiserver._config.update({"api_server": {"enabled": True, + "listen_ip_address": "0.0.0.0", + "listen_port": "8089"}}) + apiserver.run() + + assert run_mock.call_count == 1 + assert run_mock.call_args_list[0][1]["host"] == "0.0.0.0" + assert run_mock.call_args_list[0][1]["port"] == "8089" + assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) + assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", + caplog.record_tuples) + assert log_has("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json", + caplog.record_tuples) + + def test_api_reloadconf(botclient): ftbot, client = botclient @@ -353,4 +397,3 @@ def test_api_whitelist(botclient, mocker, ticker, fee, markets): assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, "method": "StaticPairList"} - From 350c9037930406691fc5091fd1138d583dca33da Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 06:24:22 +0200 Subject: [PATCH 051/134] Test falsk crash --- freqtrade/tests/rpc/test_rpc_apiserver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 41d682d2d..8752207d0 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -121,6 +121,13 @@ def test_api_run(default_conf, mocker, caplog): "e.g 127.0.0.1 in config.json", caplog.record_tuples) + # Test crashing flask + caplog.clear() + apiserver.app.run = MagicMock(side_effect=Exception) + apiserver.run() + assert log_has("Api server failed to start, exception message is:", + caplog.record_tuples) + def test_api_reloadconf(botclient): ftbot, client = botclient From b700c64dc26cc25c6393930770479982c2cec447 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 06:51:23 +0200 Subject: [PATCH 052/134] Test forcebuy - cleanup some tests --- freqtrade/rpc/api_server.py | 5 +- freqtrade/tests/rpc/test_rpc_apiserver.py | 68 +++++++++++++++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index c2d83d1fb..e7b64969f 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -314,7 +314,10 @@ class ApiServer(RPC): asset = request.json.get("pair") price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) - return self.rest_dump(trade.to_json()) + if trade: + return self.rest_dump(trade.to_json()) + else: + return self.rest_dump({"status": f"Error buying pair {asset}."}) @safe_rpc def _forcesell(self): diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 8752207d0..31a56321d 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -287,7 +287,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li } -def test_api_performance(botclient, mocker, ticker, fee, markets): +def test_api_performance(botclient, mocker, ticker, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -330,7 +330,7 @@ def test_api_performance(botclient, mocker, ticker, fee, markets): {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] -def test_api_status(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): +def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -378,7 +378,7 @@ def test_api_version(botclient): assert rc.json == {"version": __version__} -def test_api_blacklist(botclient, mocker, ticker, fee, markets): +def test_api_blacklist(botclient, mocker): ftbot, client = botclient rc = client.get("/blacklist") @@ -396,7 +396,7 @@ def test_api_blacklist(botclient, mocker, ticker, fee, markets): "method": "StaticPairList"} -def test_api_whitelist(botclient, mocker, ticker, fee, markets): +def test_api_whitelist(botclient): ftbot, client = botclient rc = client.get("/whitelist") @@ -404,3 +404,63 @@ def test_api_whitelist(botclient, mocker, ticker, fee, markets): assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, "method": "StaticPairList"} + + +def test_api_forcebuy(botclient, mocker, fee): + ftbot, client = botclient + + rc = client.post("/forcebuy", content_type='application/json', + data='{"pair": "ETH/BTC"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} + + # enable forcebuy + ftbot.config["forcebuy_enable"] = True + + fbuy_mock = MagicMock(return_value=None) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + rc = client.post("/forcebuy", content_type="application/json", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {"status": "Error buying pair ETH/BTC."} + + # Test creating trae + fbuy_mock = MagicMock(return_value=Trade( + pair='ETH/ETH', + amount=1, + exchange='bittrex', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + open_date=datetime.utcnow(), + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + )) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + + rc = client.post("/forcebuy", content_type="application/json", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {'amount': 1, + 'close_date': None, + 'close_date_hum': None, + 'close_rate': 0.265441, + 'initial_stop_loss': None, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_rate': 0.245441, + 'pair': 'ETH/ETH', + 'stake_amount': 1, + 'stop_loss': None, + 'stop_loss_pct': None, + 'trade_id': None} + + +# def test_api_sellbuy(botclient): + # TODO +# ftbot, client = botclient + + # rc = client.get("/forcesell ") From 01cd68a5aa2dbef6844845cef25c6d4a5eefc2ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 07:00:17 +0200 Subject: [PATCH 053/134] Test forcesell --- freqtrade/tests/rpc/test_rpc_apiserver.py | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 31a56321d..c3a8ab27a 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -459,8 +459,25 @@ def test_api_forcebuy(botclient, mocker, fee): 'trade_id': None} -# def test_api_sellbuy(botclient): - # TODO -# ftbot, client = botclient +def test_api_forcesell(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + patch_get_signal(ftbot, (True, False)) - # rc = client.get("/forcesell ") + rc = client.post("/forcesell", content_type="application/json", + data='{"tradeid": "1"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcesell: invalid argument"} + + ftbot.create_trade() + + rc = client.post("/forcesell", content_type="application/json", + data='{"tradeid": "1"}') + assert_response(rc) + assert rc.json == {'result': 'Created sell order for trade 1.'} From 5149ff7b120bbc431426566a708c48273c06d3b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2019 07:12:33 +0200 Subject: [PATCH 054/134] Move api to /api/v1 --- freqtrade/rpc/api_server.py | 47 ++++++++++------- freqtrade/tests/rpc/test_rpc_apiserver.py | 62 +++++++++++------------ scripts/rest_client.py | 2 +- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e7b64969f..6213256a4 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -7,12 +7,15 @@ from typing import Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder +from werkzeug.wsgi import DispatcherMiddleware from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) +BASE_URI = "/api/v1" + class ArrowJSONEncoder(JSONEncoder): def default(self, obj): @@ -60,7 +63,6 @@ class ApiServer(RPC): self._config = freqtrade.config self.app = Flask(__name__) - self.app.json_encoder = ArrowJSONEncoder # Register application handling @@ -105,29 +107,36 @@ class ApiServer(RPC): :return: """ # Actions to control the bot - self.app.add_url_rule('/start', 'start', view_func=self._start, methods=['POST']) - self.app.add_url_rule('/stop', 'stop', view_func=self._stop, methods=['POST']) - self.app.add_url_rule('/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - self.app.add_url_rule('/reload_conf', 'reload_conf', view_func=self._reload_conf, - methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/start', 'start', + view_func=self._start, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', + view_func=self._stopbuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf', + view_func=self._reload_conf, methods=['POST']) # Info commands - self.app.add_url_rule('/balance', 'balance', view_func=self._balance, methods=['GET']) - self.app.add_url_rule('/count', 'count', view_func=self._count, methods=['GET']) - self.app.add_url_rule('/daily', 'daily', view_func=self._daily, methods=['GET']) - self.app.add_url_rule('/edge', 'edge', view_func=self._edge, methods=['GET']) - self.app.add_url_rule('/profit', 'profit', view_func=self._profit, methods=['GET']) - self.app.add_url_rule('/performance', 'performance', view_func=self._performance, - methods=['GET']) - self.app.add_url_rule('/status', 'status', view_func=self._status, methods=['GET']) - self.app.add_url_rule('/version', 'version', view_func=self._version, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', + view_func=self._balance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', + view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', + view_func=self._performance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/status', 'status', + view_func=self._status, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/version', 'version', + view_func=self._version, methods=['GET']) # Combined actions and infos - self.app.add_url_rule('/blacklist', 'blacklist', view_func=self._blacklist, + self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) - self.app.add_url_rule('/whitelist', 'whitelist', view_func=self._whitelist, + self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist, methods=['GET']) - self.app.add_url_rule('/forcebuy', 'forcebuy', view_func=self._forcebuy, methods=['POST']) - self.app.add_url_rule('/forcesell', 'forcesell', view_func=self._forcesell, + self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy', + view_func=self._forcebuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) # TODO: Implement the following diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index c3a8ab27a..6233811fd 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -9,7 +9,7 @@ import pytest from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade -from freqtrade.rpc.api_server import ApiServer +from freqtrade.rpc.api_server import ApiServer, BASE_URI from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) @@ -35,35 +35,35 @@ def assert_response(response, expected_code=200): def test_api_not_found(botclient): ftbot, client = botclient - rc = client.post("/invalid_url") + rc = client.post(f"{BASE_URI}/invalid_url") assert_response(rc, 404) - assert rc.json == {'status': 'error', - 'reason': "There's no API call for http://localhost/invalid_url.", - 'code': 404 + assert rc.json == {"status": "error", + "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", + "code": 404 } def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING - rc = client.post("/stop") + rc = client.post(f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again - rc = client.post("/stop") + rc = client.post(f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot - rc = client.post("/start") + rc = client.post(f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again - rc = client.post("/start") + rc = client.post(f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'already running'} @@ -132,7 +132,7 @@ def test_api_run(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client.post("/reload_conf") + rc = client.post(f"{BASE_URI}/reload_conf") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -142,7 +142,7 @@ def test_api_stopbuy(botclient): ftbot, client = botclient assert ftbot.config['max_open_trades'] != 0 - rc = client.post("/stopbuy") + rc = client.post(f"{BASE_URI}/stopbuy") assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 @@ -172,7 +172,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) - rc = client.get("/balance") + rc = client.get(f"{BASE_URI}/balance") assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 @@ -195,7 +195,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/count") + rc = client.get(f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 0 @@ -203,7 +203,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() - rc = client.get("/count") + rc = client.get(f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -219,7 +219,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/daily") + rc = client.get(f"{BASE_URI}/daily") assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) @@ -235,7 +235,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get("/edge") + rc = client.get(f"{BASE_URI}/edge") assert_response(rc, 502) assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} @@ -251,7 +251,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li markets=PropertyMock(return_value=markets) ) - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc, 502) assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -261,7 +261,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc, 502) assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -270,7 +270,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li trade.close_date = datetime.utcnow() trade.is_open = False - rc = client.get("/profit") + rc = client.get(f"{BASE_URI}/profit") assert_response(rc) assert rc.json == {'avg_duration': '0:00:00', 'best_pair': 'ETH/BTC', @@ -323,7 +323,7 @@ def test_api_performance(botclient, mocker, ticker, fee): Trade.session.add(trade) Trade.session.flush() - rc = client.get("/performance") + rc = client.get(f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json) == 2 assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, @@ -341,12 +341,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) - rc = client.get("/status") + rc = client.get(f"{BASE_URI}/status") assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} ftbot.create_trade() - rc = client.get("/status") + rc = client.get(f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 90.99181074, @@ -373,7 +373,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_version(botclient): ftbot, client = botclient - rc = client.get("/version") + rc = client.get(f"{BASE_URI}/version") assert_response(rc) assert rc.json == {"version": __version__} @@ -381,14 +381,14 @@ def test_api_version(botclient): def test_api_blacklist(botclient, mocker): ftbot, client = botclient - rc = client.get("/blacklist") + rc = client.get(f"{BASE_URI}/blacklist") assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, "method": "StaticPairList"} # Add ETH/BTC to blacklist - rc = client.post("/blacklist", data='{"blacklist": ["ETH/BTC"]}', + rc = client.post(f"{BASE_URI}/blacklist", data='{"blacklist": ["ETH/BTC"]}', content_type='application/json') assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], @@ -399,7 +399,7 @@ def test_api_blacklist(botclient, mocker): def test_api_whitelist(botclient): ftbot, client = botclient - rc = client.get("/whitelist") + rc = client.get(f"{BASE_URI}/whitelist") assert_response(rc) assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, @@ -409,7 +409,7 @@ def test_api_whitelist(botclient): def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient - rc = client.post("/forcebuy", content_type='application/json', + rc = client.post(f"{BASE_URI}/forcebuy", content_type='application/json', data='{"pair": "ETH/BTC"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} @@ -419,7 +419,7 @@ def test_api_forcebuy(botclient, mocker, fee): fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post("/forcebuy", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {"status": "Error buying pair ETH/BTC."} @@ -440,7 +440,7 @@ def test_api_forcebuy(botclient, mocker, fee): )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post("/forcebuy", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, @@ -470,14 +470,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): ) patch_get_signal(ftbot, (True, False)) - rc = client.post("/forcesell", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} ftbot.create_trade() - rc = client.post("/forcesell", content_type="application/json", + rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 81c4b66cc..54b08a03a 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -36,7 +36,7 @@ class FtRestClient(): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError('invalid method <{0}>'.format(method)) - basepath = f"{self._serverurl}/{apipath}" + basepath = f"{self._serverurl}/api/v1/{apipath}" hd = {"Accept": "application/json", "Content-Type": "application/json" From 540d4bef1e146920dce1215441e951f8a70992cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:50:19 +0200 Subject: [PATCH 055/134] gracefully shutdown flask --- freqtrade/rpc/api_server.py | 51 +++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 6213256a4..3e1bf6195 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -7,7 +7,7 @@ from typing import Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder -from werkzeug.wsgi import DispatcherMiddleware +from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException @@ -74,8 +74,31 @@ class ApiServer(RPC): def cleanup(self) -> None: logger.info("Stopping API Server") - # TODO: Gracefully shutdown - right now it'll fail on /reload_conf - # since it's not terminated correctly. + self.srv.shutdown() + + def run(self): + """ + Method that runs flask app in its own thread forever. + Section to handle configuration and running of the Rest server + also to check and warn if not bound to a loopback, warn on security risk. + """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] + + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + if not IPv4Address(rest_ip).is_loopback: + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + # Run the Server + logger.info('Starting Local Rest Server') + try: + self.srv = make_server(rest_ip, rest_port, self.app) + self.srv.serve_forever() + except Exception: + logger.exception("Api server failed to start, exception message is:") + logger.info('Starting Local Rest Server_end') def send_msg(self, msg: Dict[str, str]) -> None: """ @@ -142,28 +165,6 @@ class ApiServer(RPC): # TODO: Implement the following # help (?) - def run(self): - """ Method that runs flask app in its own thread forever. - Section to handle configuration and running of the Rest server - also to check and warn if not bound to a loopback, warn on security risk. - """ - rest_ip = self._config['api_server']['listen_ip_address'] - rest_port = self._config['api_server']['listen_port'] - - logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') - if not IPv4Address(rest_ip).is_loopback: - logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") - logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") - - # Run the Server - logger.info('Starting Local Rest Server') - try: - self.app.run(host=rest_ip, port=rest_port) - except Exception: - logger.exception("Api server failed to start, exception message is:") - logger.info('Starting Local Rest Server_end') - def page_not_found(self, error): """ Return "404 not found", 404. From bfc57a6f6d3e076ebd02ec0bd68459429604d632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:50:29 +0200 Subject: [PATCH 056/134] Adapt tests to new method of starting flask --- freqtrade/tests/rpc/test_rpc_apiserver.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 6233811fd..1842d8828 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -6,10 +6,11 @@ from datetime import datetime from unittest.mock import ANY, MagicMock, PropertyMock import pytest +from flask import Flask from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade -from freqtrade.rpc.api_server import ApiServer, BASE_URI +from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) @@ -86,18 +87,17 @@ def test_api_run(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) - - # Monkey patch flask app run_mock = MagicMock() - apiserver.app = MagicMock() - apiserver.app.run = run_mock + mocker.patch('freqtrade.rpc.api_server.make_server', run_mock) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) assert apiserver._config == default_conf apiserver.run() assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][1]["host"] == "127.0.0.1" - assert run_mock.call_args_list[0][1]["port"] == "8080" + assert run_mock.call_args_list[0][0][0] == "127.0.0.1" + assert run_mock.call_args_list[0][0][1] == "8080" + assert isinstance(run_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) @@ -111,8 +111,9 @@ def test_api_run(default_conf, mocker, caplog): apiserver.run() assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][1]["host"] == "0.0.0.0" - assert run_mock.call_args_list[0][1]["port"] == "8089" + assert run_mock.call_args_list[0][0][0] == "0.0.0.0" + assert run_mock.call_args_list[0][0][1] == "8089" + assert isinstance(run_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -123,7 +124,7 @@ def test_api_run(default_conf, mocker, caplog): # Test crashing flask caplog.clear() - apiserver.app.run = MagicMock(side_effect=Exception) + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() assert log_has("Api server failed to start, exception message is:", caplog.record_tuples) From fd5012c04e4c92cdfab04102d85099abb30e282e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 09:54:36 +0200 Subject: [PATCH 057/134] Add test for api cleanup --- freqtrade/tests/rpc/test_rpc_apiserver.py | 42 +++++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 1842d8828..123988288 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -87,33 +87,34 @@ def test_api_run(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.make_server', run_mock) + server_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) assert apiserver._config == default_conf apiserver.run() - assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][0][0] == "127.0.0.1" - assert run_mock.call_args_list[0][0][1] == "8080" - assert isinstance(run_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "127.0.0.1" + assert server_mock.call_args_list[0][0][1] == "8080" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert hasattr(apiserver, "srv") assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) # Test binding to public caplog.clear() - run_mock.reset_mock() + server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", "listen_port": "8089"}}) apiserver.run() - assert run_mock.call_count == 1 - assert run_mock.call_args_list[0][0][0] == "0.0.0.0" - assert run_mock.call_args_list[0][0][1] == "8089" - assert isinstance(run_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "0.0.0.0" + assert server_mock.call_args_list[0][0][1] == "8089" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) assert log_has("Starting Local Rest Server", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -130,6 +131,25 @@ def test_api_run(default_conf, mocker, caplog): caplog.record_tuples) +def test_api_cleanup(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver.run() + stop_mock = MagicMock() + stop_mock.shutdown = MagicMock() + apiserver.srv = stop_mock + + apiserver.cleanup() + assert stop_mock.shutdown.call_count == 1 + assert log_has("Stopping API Server", caplog.record_tuples) + + def test_api_reloadconf(botclient): ftbot, client = botclient From c272e1ccdf1d94bee785e211713bae2aedda84d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:24:01 +0200 Subject: [PATCH 058/134] Add default rest config --- config_full.json.example | 5 +++++ scripts/rest_client.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 4c4ad3c58..6603540cf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -109,6 +109,11 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080 + }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", "forcebuy_enable": false, diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 54b08a03a..4bf46e6fd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -234,17 +234,19 @@ def load_config(configfile): return {} +def print_commands(): + # Print dynamic help for the different commands + client = FtRestClient(None) + print("Possible commands:") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + print(f"{x} {getattr(client, x).__doc__}") + + def main(args): - if args.get("show"): - # Print dynamic help for the different commands - client = FtRestClient(None) - print("Possible commands:") - for x, y in inspect.getmembers(client): - if not x.startswith('_'): - print(f"{x} {getattr(client, x).__doc__}") - - return + if args.get("help"): + print_commands() config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") @@ -256,6 +258,7 @@ def main(args): command = args["command"] if command not in m: logger.error(f"Command {command} not defined") + print_commands() return print(getattr(client, command)(*args["command_arguments"])) From 70fabebcb324f38e80ce15dae7b197c2f75c3d5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:24:22 +0200 Subject: [PATCH 059/134] Document rest api --- docs/rest-api.md | 184 +++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 185 insertions(+) create mode 100644 docs/rest-api.md diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 000000000..aeb1421c1 --- /dev/null +++ b/docs/rest-api.md @@ -0,0 +1,184 @@ +# REST API Usage + +## Configuration + +Enable the rest API by adding the api_server section to your configuration and setting `api_server.enabled` to `true`. + +Sample configuration: + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080 + }, +``` + +!!! Danger: Security warning + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will be able to control your bot. + +You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. + + +### Configuration with docker + +If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + }, +``` + +Add the following to your docker command: + +``` bash + -p 127.0.0.1:8080:8080 +``` + +A complete sample-command may then look as follows: + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -p 127.0.0.1:8080:8080 \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Danger "Security warning" + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others will be able to control your bot. + +## Consuming the API + +You can consume the API by using the script `scripts/rest_client.py`. +The client script only requires the `requests` module, so FreqTrade does not need to be installed on the system. + +``` bash +python3 scripts/rest_client.py [optional parameters] +``` + +By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. + +### Minimalistic client config + +``` json +{ + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + } +} +``` + +``` bash +python3 scripts/rest_client.py --config rest_config.json [optional parameters] +``` + +## Available commands + +| Command | Default | Description | +|----------|---------|-------------| +| `start` | | Starts the trader +| `stop` | | Stops the trader +| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. +| `reload_conf` | | Reloads the configuration file +| `status` | | Lists all open trades +| `status table` | | List all open trades in a table format +| `count` | | Displays number of trades used and available +| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance +| `forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). +| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). +| `forcebuy [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `performance` | | Show performance of each finished trade grouped by pair +| `balance` | | Show account balance per currency +| `daily ` | 7 | Shows profit or loss per day, over the last n days +| `whitelist` | | Show the current whitelist +| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. +| `edge` | | Show validated pairs by Edge if it is enabled. +| `version` | | Show version + +Possible commands can be listed from the rest-client script using the `help` command. + +``` bash +python3 scripts/rest_client.py help +``` + +``` output +Possible commands: +balance + Get the account balance + :returns: json object + +blacklist + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: json object + +count + Returns the amount of open trades + :returns: json object + +daily + Returns the amount of open trades + :returns: json object + +edge + Returns information about edge + :returns: json object + +forcebuy + Buy an asset + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :returns: json object of the trade + +forcesell + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + +performance + Returns the performance of the different coins + :returns: json object + +profit + Returns the profit summary + :returns: json object + +reload_conf + Reload configuration + :returns: json object + +start + Start the bot if it's in stopped state. + :returns: json object + +status + Get the status of open trades + :returns: json object + +stop + Stop the bot. Use start to restart + :returns: json object + +stopbuy + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + +version + Returns the version of the bot + :returns: json object containing the version + +whitelist + Show the current whitelist + :returns: json object + + +``` diff --git a/mkdocs.yml b/mkdocs.yml index ecac265c1..547c527a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md + - REST API: rest-api.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge positioning: edge.md From f2e4689d0c0fb4afb007233752c9587c581288b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:29:38 +0200 Subject: [PATCH 060/134] Cleanup script --- docs/rest-api.md | 3 --- scripts/rest_client.py | 8 +------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index aeb1421c1..728941c88 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -19,7 +19,6 @@ Sample configuration: You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. - ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. @@ -179,6 +178,4 @@ version whitelist Show the current whitelist :returns: json object - - ``` diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4bf46e6fd..0669feb8c 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -216,12 +216,6 @@ def add_arguments(): ) args = parser.parse_args() - # if len(argv) == 1: - # print('\nThis script accepts the following arguments') - # print('- daily (int) - Where int is the number of days to report back. daily 3') - # print('- start - this will start the trading thread') - # print('- stop - this will start the trading thread') - # print('- there will be more....\n') return vars(args) @@ -235,7 +229,7 @@ def load_config(configfile): def print_commands(): - # Print dynamic help for the different commands + # Print dynamic help for the different commands using the commands doc-strings client = FtRestClient(None) print("Possible commands:") for x, y in inspect.getmembers(client): From 9385a27ff031d9f461a47f2d12ef3a6b95b85dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:34:30 +0200 Subject: [PATCH 061/134] Sort imports --- 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 3e1bf6195..fca7fa702 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,6 +1,6 @@ import logging import threading -from datetime import datetime, date +from datetime import date, datetime from ipaddress import IPv4Address from typing import Dict From 79cac36b34d0dde28a0bc6d56a0735f8cedf3b52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 10:36:40 +0200 Subject: [PATCH 062/134] Reference reest api in main documentation page --- docs/index.md | 12 ++++++++---- scripts/rest_client.py | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index 9abc71747..9fbc0519c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,8 @@ Freqtrade is a cryptocurrency trading bot written in Python. We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it. - ## Features + - Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux. - Persistence: Persistence is achieved through sqlite database. - Dry-run mode: Run the bot without playing money. @@ -31,17 +31,19 @@ Freqtrade is a cryptocurrency trading bot written in Python. - Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume. - Blacklist crypto-currencies: Select which crypto-currency you want to avoid. - - Manageable via Telegram: Manage the bot with Telegram. + - Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API. - Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### Up to date clock + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -56,12 +59,13 @@ To run this bot we recommend you a cloud instance with a minimum of: - virtualenv (Recommended) - Docker (Recommended) - ## Support + Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. ## Ready to try? + Begin by reading our installation guide [here](installation). diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 0669feb8c..b31a1de50 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -11,7 +11,6 @@ import argparse import json import logging import inspect -from typing import List from urllib.parse import urlencode, urlparse, urlunparse from pathlib import Path From e6ae890defcbf32ddfe1fb990e5ed3f5a948c6eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 13:36:51 +0200 Subject: [PATCH 063/134] small adjustments after first feedback --- docs/rest-api.md | 4 ++-- freqtrade/rpc/api_server.py | 16 +++++----------- freqtrade/tests/rpc/test_rpc_apiserver.py | 6 +++--- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 728941c88..95eec3020 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -15,7 +15,7 @@ Sample configuration: ``` !!! Danger: Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will be able to control your bot. + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will potentially be able to control your bot. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. @@ -50,7 +50,7 @@ docker run -d \ ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others will be able to control your bot. + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. ## Consuming the API diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index fca7fa702..923605e7b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -66,7 +66,6 @@ class ApiServer(RPC): self.app.json_encoder = ArrowJSONEncoder # Register application handling - self.register_rest_other() self.register_rest_rpc_urls() thread = threading.Thread(target=self.run, daemon=True) @@ -92,13 +91,13 @@ class ApiServer(RPC): "e.g 127.0.0.1 in config.json") # Run the Server - logger.info('Starting Local Rest Server') + logger.info('Starting Local Rest Server.') try: self.srv = make_server(rest_ip, rest_port, self.app) self.srv.serve_forever() except Exception: - logger.exception("Api server failed to start, exception message is:") - logger.info('Starting Local Rest Server_end') + logger.exception("Api server failed to start.") + logger.info('Local Rest Server started.') def send_msg(self, msg: Dict[str, str]) -> None: """ @@ -114,13 +113,6 @@ class ApiServer(RPC): def rest_error(self, error_msg): return jsonify({"error": error_msg}), 502 - def register_rest_other(self): - """ - Registers flask app URLs that are not calls to functionality in rpc.rpc. - :return: - """ - self.app.register_error_handler(404, self.page_not_found) - def register_rest_rpc_urls(self): """ Registers flask app URLs that are calls to functonality in rpc.rpc. @@ -129,6 +121,8 @@ class ApiServer(RPC): Label can be used as a shortcut when refactoring :return: """ + self.app.register_error_handler(404, self.page_not_found) + # Actions to control the bot self.app.add_url_rule(f'{BASE_URI}/start', 'start', view_func=self._start, methods=['POST']) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 123988288..5b9e538b5 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -101,7 +101,7 @@ def test_api_run(default_conf, mocker, caplog): assert hasattr(apiserver, "srv") assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) - assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) # Test binding to public caplog.clear() @@ -116,7 +116,7 @@ def test_api_run(default_conf, mocker, caplog): assert server_mock.call_args_list[0][0][1] == "8089" assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) - assert log_has("Starting Local Rest Server", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", caplog.record_tuples) assert log_has("SECURITY WARNING - This is insecure please set to your loopback," @@ -127,7 +127,7 @@ def test_api_run(default_conf, mocker, caplog): caplog.clear() mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() - assert log_has("Api server failed to start, exception message is:", + assert log_has("Api server failed to start.", caplog.record_tuples) From 2cf07e218590182530f2614d96ac245e682e6064 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 May 2019 13:39:12 +0200 Subject: [PATCH 064/134] rename exception handlers --- freqtrade/rpc/api_server.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 923605e7b..6792cc9a0 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -41,7 +41,7 @@ class ApiServer(RPC): This class starts a none blocking thread the api server runs within """ - def safe_rpc(func): + def rpc_catch_errors(func): def func_wrapper(self, *args, **kwargs): @@ -169,6 +169,7 @@ class ApiServer(RPC): 'code': 404 }), 404 + @rpc_catch_errors def _start(self): """ Handler for /start. @@ -177,6 +178,7 @@ class ApiServer(RPC): msg = self._rpc_start() return self.rest_dump(msg) + @rpc_catch_errors def _stop(self): """ Handler for /stop. @@ -185,6 +187,7 @@ class ApiServer(RPC): msg = self._rpc_stop() return self.rest_dump(msg) + @rpc_catch_errors def _stopbuy(self): """ Handler for /stopbuy. @@ -193,12 +196,14 @@ class ApiServer(RPC): msg = self._rpc_stopbuy() return self.rest_dump(msg) + @rpc_catch_errors def _version(self): """ Prints the bot's version """ return self.rest_dump({"version": __version__}) + @rpc_catch_errors def _reload_conf(self): """ Handler for /reload_conf. @@ -207,7 +212,7 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) - @safe_rpc + @rpc_catch_errors def _count(self): """ Handler for /count. @@ -216,7 +221,7 @@ class ApiServer(RPC): msg = self._rpc_count() return self.rest_dump(msg) - @safe_rpc + @rpc_catch_errors def _daily(self): """ Returns the last X days trading stats summary. @@ -233,7 +238,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _edge(self): """ Returns information related to Edge. @@ -243,7 +248,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _profit(self): """ Handler for /profit. @@ -259,7 +264,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _performance(self): """ Handler for /performance. @@ -273,7 +278,7 @@ class ApiServer(RPC): return self.rest_dump(stats) - @safe_rpc + @rpc_catch_errors def _status(self): """ Handler for /status. @@ -283,7 +288,7 @@ class ApiServer(RPC): results = self._rpc_trade_status() return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _balance(self): """ Handler for /balance. @@ -293,7 +298,7 @@ class ApiServer(RPC): results = self._rpc_balance(self._config.get('fiat_display_currency', '')) return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _whitelist(self): """ Handler for /whitelist. @@ -301,7 +306,7 @@ class ApiServer(RPC): results = self._rpc_whitelist() return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _blacklist(self): """ Handler for /blacklist. @@ -310,7 +315,7 @@ class ApiServer(RPC): results = self._rpc_blacklist(add) return self.rest_dump(results) - @safe_rpc + @rpc_catch_errors def _forcebuy(self): """ Handler for /forcebuy. @@ -323,7 +328,7 @@ class ApiServer(RPC): else: return self.rest_dump({"status": f"Error buying pair {asset}."}) - @safe_rpc + @rpc_catch_errors def _forcesell(self): """ Handler for /forcesell. From 11dce9128193184623d9a4896d503c351ef37faf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 21 May 2019 20:49:02 +0300 Subject: [PATCH 065/134] data/history minor cleanup --- freqtrade/data/history.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 86d3c3071..27e68b533 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -63,12 +63,8 @@ def load_tickerdata_file( Load a pair from file, either .json.gz or .json :return tickerlist or None if unsuccesful """ - path = make_testdata_path(datadir) - pair_s = pair.replace('/', '_') - file = path.joinpath(f'{pair_s}-{ticker_interval}.json') - - pairdata = misc.file_load_json(file) - + filename = pair_data_filename(datadir, pair, ticker_interval) + pairdata = misc.file_load_json(filename) if not pairdata: return None @@ -142,11 +138,18 @@ def load_data(datadir: Optional[Path], return result -def make_testdata_path(datadir: Optional[Path]) -> Path: +def make_datadir_path(datadir: Optional[Path]) -> Path: """Return the path where testdata files are stored""" return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() +def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path: + path = make_datadir_path(datadir) + pair_s = pair.replace("/", "_") + filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') + return filename + + def load_cached_data_for_updating(filename: Path, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: @@ -209,9 +212,7 @@ def download_pair_history(datadir: Optional[Path], ) try: - path = make_testdata_path(datadir) - filepair = pair.replace("/", "_") - filename = path.joinpath(f'{filepair}-{ticker_interval}.json') + filename = pair_data_filename(datadir, pair, ticker_interval) logger.info( f'Download history data for pair: "{pair}", interval: {ticker_interval} ' @@ -236,8 +237,9 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True - except Exception: + except Exception as e: logger.error( - f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}.' + f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' + f'Error: {e}' ) return False From 7cb753754b039898c3cfae2dd8049068cda6b4a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 21 May 2019 20:49:19 +0300 Subject: [PATCH 066/134] tests adjusted --- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/data/test_history.py | 7 ++++--- freqtrade/tests/test_misc.py | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index dd7cbe0d9..4c0426b93 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -2,12 +2,12 @@ import pytest from pandas import DataFrame from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.data.history import make_testdata_path +from freqtrade.data.history import make_datadir_path def test_load_backtest_data(): - filename = make_testdata_path(None) / "backtest-result_test.json" + filename = make_datadir_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"] diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 15442f577..a37b42351 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, load_tickerdata_file, - make_testdata_path, + make_datadir_path, trim_tickerlist) from freqtrade.misc import file_dump_json from freqtrade.tests.conftest import get_patched_exchange, log_has @@ -136,7 +136,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau def test_testdata_path() -> None: - assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) + assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_datadir_path(None)) def test_load_cached_data_for_updating(mocker) -> None: @@ -321,7 +321,8 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def _clean_test_file(file1_1) _clean_test_file(file1_5) assert log_has( - 'Failed to download history data for pair: "MEME/BTC", interval: 1m.', + 'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' + 'Error: File Error', caplog.record_tuples ) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 2da6b8718..c7bcf7edf 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, file_dump_json, file_load_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file, make_testdata_path +from freqtrade.data.history import load_tickerdata_file, pair_data_filename from freqtrade.strategy.default_strategy import DefaultStrategy @@ -60,13 +60,13 @@ def test_file_dump_json(mocker) -> None: def test_file_load_json(mocker) -> None: # 7m .json does not exist - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '7m')) assert not ret # 1m json exists (but no .gz exists) - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '1m')) assert ret # 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '8m')) assert ret From 98eeec31451c844bbdabc0a8c8dacd2474122596 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:04:58 +0300 Subject: [PATCH 067/134] renaming of make_testdata_path reverted --- freqtrade/data/history.py | 4 ++-- freqtrade/tests/data/test_btanalysis.py | 4 ++-- freqtrade/tests/data/test_history.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 27e68b533..3bec63926 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -138,13 +138,13 @@ def load_data(datadir: Optional[Path], return result -def make_datadir_path(datadir: Optional[Path]) -> Path: +def make_testdata_path(datadir: Optional[Path]) -> Path: """Return the path where testdata files are stored""" return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path: - path = make_datadir_path(datadir) + path = make_testdata_path(datadir) pair_s = pair.replace("/", "_") filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') return filename diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index 4c0426b93..dd7cbe0d9 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -2,12 +2,12 @@ import pytest from pandas import DataFrame from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.data.history import make_datadir_path +from freqtrade.data.history import make_testdata_path def test_load_backtest_data(): - filename = make_datadir_path(None) / "backtest-result_test.json" + filename = make_testdata_path(None) / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"] diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index a37b42351..0d4210d3a 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, load_tickerdata_file, - make_datadir_path, + make_testdata_path, trim_tickerlist) from freqtrade.misc import file_dump_json from freqtrade.tests.conftest import get_patched_exchange, log_has @@ -136,7 +136,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau def test_testdata_path() -> None: - assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_datadir_path(None)) + assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) def test_load_cached_data_for_updating(mocker) -> None: From 2c9a519c5e359ba2fea353bfd29155c06c8a2d3b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:21:36 +0300 Subject: [PATCH 068/134] edge: handle properly the 'No trades' case --- freqtrade/edge/__init__.py | 1 + freqtrade/optimize/edge_cli.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4801c6cb3..5c7252d88 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -139,6 +139,7 @@ class Edge(): # If no trade found then exit if len(trades) == 0: + logger.info("No trades created.") return False # Fill missing, calculable columns, profit, duration , abs etc. diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 9b628cf2e..818c1e050 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -73,9 +73,10 @@ class EdgeCli(object): floatfmt=floatfmt, tablefmt="pipe") def start(self) -> None: - self.edge.calculate() - print('') # blank like for readability - print(self._generate_edge_table(self.edge._cached_pairs)) + result = self.edge.calculate() + if result: + print('') # blank like for readability + print(self._generate_edge_table(self.edge._cached_pairs)) def setup_configuration(args: Namespace) -> Dict[str, Any]: From 406e266bb4ea12caaaaf86b6ddde5369e7450fea Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 14:34:35 +0300 Subject: [PATCH 069/134] typo in comment fixed --- freqtrade/optimize/edge_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 818c1e050..d37b930b8 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -75,7 +75,7 @@ class EdgeCli(object): def start(self) -> None: result = self.edge.calculate() if result: - print('') # blank like for readability + print('') # blank line for readability print(self._generate_edge_table(self.edge._cached_pairs)) From 6e1da13920eb3b1ed7ce501cf0c13f5c11a667df Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 May 2019 17:19:11 +0300 Subject: [PATCH 070/134] Log message changed --- freqtrade/edge/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 5c7252d88..053be6bc3 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -139,7 +139,7 @@ class Edge(): # If no trade found then exit if len(trades) == 0: - logger.info("No trades created.") + logger.info("No trades found.") return False # Fill missing, calculable columns, profit, duration , abs etc. From 7b074765ab059193ec75cacd8739d5e7e5ea6204 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 May 2019 19:48:22 +0200 Subject: [PATCH 071/134] Improve edge tests - cleanup test file --- freqtrade/tests/edge/test_edge.py | 128 ++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 41 deletions(-) diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index af8674188..a14e3282e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -10,10 +10,11 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime +from freqtrade import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import get_patched_freqtradebot +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has from freqtrade.tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -30,7 +31,50 @@ ticker_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} +# Helpers for this test file + +def _validate_ohlc(buy_ohlc_sell_matrice): + for index, ohlc in enumerate(buy_ohlc_sell_matrice): + # if not high < open < low or not high < close < low + if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: + raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') + return True + + +def _build_dataframe(buy_ohlc_sell_matrice): + _validate_ohlc(buy_ohlc_sell_matrice) + tickers = [] + for ohlc in buy_ohlc_sell_matrice: + ticker = { + 'date': ticker_start_time.shift( + minutes=( + ohlc[0] * + ticker_interval_in_minute)).timestamp * + 1000, + 'buy': ohlc[1], + 'open': ohlc[2], + 'high': ohlc[3], + 'low': ohlc[4], + 'close': ohlc[5], + 'sell': ohlc[6]} + tickers.append(ticker) + + frame = DataFrame(tickers) + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) + + return frame + + +def _time_on_candle(number): + return np.datetime64(ticker_start_time.shift( + minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') + + +# End helper functions # Open trade should be removed from the end tc0 = BTContainer(data=[ # D O H L C V B S @@ -203,46 +247,6 @@ def test_nonexisting_stake_amount(mocker, edge_conf): assert edge.stake_amount('N/O', 1, 2, 1) == 0.15 -def _validate_ohlc(buy_ohlc_sell_matrice): - for index, ohlc in enumerate(buy_ohlc_sell_matrice): - # if not high < open < low or not high < close < low - if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: - raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') - return True - - -def _build_dataframe(buy_ohlc_sell_matrice): - _validate_ohlc(buy_ohlc_sell_matrice) - tickers = [] - for ohlc in buy_ohlc_sell_matrice: - ticker = { - 'date': ticker_start_time.shift( - minutes=( - ohlc[0] * - ticker_interval_in_minute)).timestamp * - 1000, - 'buy': ohlc[1], - 'open': ohlc[2], - 'high': ohlc[3], - 'low': ohlc[4], - 'close': ohlc[5], - 'sell': ohlc[6]} - tickers.append(ticker) - - frame = DataFrame(tickers) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) - - return frame - - -def _time_on_candle(number): - return np.datetime64(ticker_start_time.shift( - minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') - - def test_edge_heartbeat_calculate(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -298,6 +302,40 @@ def test_edge_process_downloaded_data(mocker, edge_conf): assert edge._last_updated <= arrow.utcnow().timestamp + 2 +def test_edge_process_no_data(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No data found. Edge is stopped ...", caplog.record_tuples) + assert edge._last_updated == 0 + + +def test_edge_process_no_trades(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No trades found.", caplog.record_tuples) + + +def test_edge_init_error(mocker, edge_conf,): + edge_conf['stake_amount'] = 0.5 + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'): + get_patched_freqtradebot(mocker, edge_conf) + + def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) @@ -360,3 +398,11 @@ def test_process_expectancy(mocker, edge_conf): assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384 assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0 assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128 + + # Pop last item so no trade is profitable + trades.pop() + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + assert len(final) == 0 + assert isinstance(final, dict) From 253025c0feb173ccb304ca3d38dcfc2ac7b28477 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 May 2019 19:53:42 +0200 Subject: [PATCH 072/134] Add tests for check_int_positive --- freqtrade/tests/test_arguments.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 0952d1c5d..d71502abb 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -185,3 +185,12 @@ def test_testdata_dl_options() -> None: assert args.export == 'export/folder' assert args.days == 30 assert args.exchange == 'binance' + + +def test_check_int_positive() -> None: + assert Arguments.check_int_positive(2) == 2 + assert Arguments.check_int_positive(6) == 6 + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive(-6) + + assert Arguments.check_int_positive(2.5) == 2 From 7b968a2401dc0b82d0c2e32016f16d9c19185173 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 24 May 2019 04:04:07 +0300 Subject: [PATCH 073/134] logger.exception cleanup --- freqtrade/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 79d150441..809ab3c7a 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -54,8 +54,8 @@ def main(sysargv: List[str]) -> None: except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException as e: - logger.exception('Fatal exception! ' + str(e)) + except BaseException: + logger.exception('Fatal exception!') finally: if worker: worker.exit() From 7bbe8b24832099ca7bac9508373c232c4497f683 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 May 2019 06:22:27 +0200 Subject: [PATCH 074/134] Add a few more testcases for check_int_positive --- freqtrade/tests/test_arguments.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d71502abb..455f3dbc6 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import argparse import pytest @@ -194,3 +193,9 @@ def test_check_int_positive() -> None: Arguments.check_int_positive(-6) assert Arguments.check_int_positive(2.5) == 2 + assert Arguments.check_int_positive("3") == 3 + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("3.5") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("DeadBeef") From c3e93e7593b7f092e80464ebad25918420246bd3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 24 May 2019 23:08:56 +0300 Subject: [PATCH 075/134] fix reduce() TypeError in hyperopts --- docs/hyperopt.md | 7 ++++--- freqtrade/optimize/default_hyperopt.py | 14 ++++++++------ user_data/hyperopts/sample_hyperopt.py | 14 ++++++++------ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b4e42de16..79ea4771b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -122,9 +122,10 @@ So let's write the buy strategy using these values: dataframe['macd'], dataframe['macdsignal'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 721848d2e..7f1cb2435 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -70,9 +70,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -129,9 +130,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 54f65a7e6..7cb55378e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -79,9 +79,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -138,9 +139,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe From 469c0b6a558d3194026c97ec637f47d1e4e06eae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 13:16:00 +0200 Subject: [PATCH 076/134] Adjust check_int_positive tests --- freqtrade/arguments.py | 2 +- freqtrade/tests/test_arguments.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 327915b61..5afa8fa06 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -405,7 +405,7 @@ class Arguments(object): raise Exception('Incorrect syntax for timerange "%s"' % text) @staticmethod - def check_int_positive(value) -> int: + def check_int_positive(value: str) -> int: try: uint = int(value) if uint <= 0: diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 455f3dbc6..ecd108b5e 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -187,13 +187,17 @@ def test_testdata_dl_options() -> None: def test_check_int_positive() -> None: - assert Arguments.check_int_positive(2) == 2 - assert Arguments.check_int_positive(6) == 6 - with pytest.raises(argparse.ArgumentTypeError): - Arguments.check_int_positive(-6) - assert Arguments.check_int_positive(2.5) == 2 assert Arguments.check_int_positive("3") == 3 + assert Arguments.check_int_positive("1") == 1 + assert Arguments.check_int_positive("100") == 100 + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("-2") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("0") + with pytest.raises(argparse.ArgumentTypeError): Arguments.check_int_positive("3.5") From 7e952b028a6d047f0da4ce83cf0bcea2272bf582 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:11:30 +0200 Subject: [PATCH 077/134] Add basic auth to rest-api --- config_full.json.example | 4 +++- freqtrade/rpc/api_server.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 6603540cf..acecfb649 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -112,7 +112,9 @@ "api_server": { "enabled": false, "listen_ip_address": "127.0.0.1", - "listen_port": 8080 + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 6792cc9a0..5e76e148c 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -53,6 +53,19 @@ class ApiServer(RPC): return func_wrapper + def require_login(func): + + def func_wrapper(self, *args, **kwargs): + # Also works if no username/password is specified + if (request.headers.get('username') == self._config['api_server'].get('username') + and request.headers.get('password') == self._config['api_server'].get('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 @@ -159,6 +172,7 @@ class ApiServer(RPC): # TODO: Implement the following # help (?) + @require_login def page_not_found(self, error): """ Return "404 not found", 404. @@ -169,6 +183,7 @@ class ApiServer(RPC): 'code': 404 }), 404 + @require_login @rpc_catch_errors def _start(self): """ @@ -178,6 +193,7 @@ class ApiServer(RPC): msg = self._rpc_start() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _stop(self): """ @@ -187,6 +203,7 @@ class ApiServer(RPC): msg = self._rpc_stop() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _stopbuy(self): """ @@ -196,6 +213,7 @@ class ApiServer(RPC): msg = self._rpc_stopbuy() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _version(self): """ @@ -203,6 +221,7 @@ class ApiServer(RPC): """ return self.rest_dump({"version": __version__}) + @require_login @rpc_catch_errors def _reload_conf(self): """ @@ -212,6 +231,7 @@ class ApiServer(RPC): msg = self._rpc_reload_conf() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _count(self): """ @@ -221,6 +241,7 @@ class ApiServer(RPC): msg = self._rpc_count() return self.rest_dump(msg) + @require_login @rpc_catch_errors def _daily(self): """ @@ -238,6 +259,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _edge(self): """ @@ -248,6 +270,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _profit(self): """ @@ -264,6 +287,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _performance(self): """ @@ -278,6 +302,7 @@ class ApiServer(RPC): return self.rest_dump(stats) + @require_login @rpc_catch_errors def _status(self): """ @@ -288,6 +313,7 @@ class ApiServer(RPC): results = self._rpc_trade_status() return self.rest_dump(results) + @require_login @rpc_catch_errors def _balance(self): """ @@ -298,6 +324,7 @@ class ApiServer(RPC): results = self._rpc_balance(self._config.get('fiat_display_currency', '')) return self.rest_dump(results) + @require_login @rpc_catch_errors def _whitelist(self): """ @@ -306,6 +333,7 @@ class ApiServer(RPC): results = self._rpc_whitelist() return self.rest_dump(results) + @require_login @rpc_catch_errors def _blacklist(self): """ @@ -315,6 +343,7 @@ class ApiServer(RPC): results = self._rpc_blacklist(add) return self.rest_dump(results) + @require_login @rpc_catch_errors def _forcebuy(self): """ @@ -328,6 +357,7 @@ class ApiServer(RPC): else: return self.rest_dump({"status": f"Error buying pair {asset}."}) + @require_login @rpc_catch_errors def _forcesell(self): """ From 04c35b465ed20284763227c0d28a0f6471e6713b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:13:59 +0200 Subject: [PATCH 078/134] Add authorization to tests --- freqtrade/tests/rpc/test_rpc_apiserver.py | 111 ++++++++++++++++------ 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 5b9e538b5..620a7c34b 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -16,11 +16,19 @@ from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_get_signal) +_TEST_USER = "FreqTrader" +_TEST_PASS = "SuperSecurePassword1!" + + @pytest.fixture def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": "8080", + "username": _TEST_USER, + "password": _TEST_PASS, + }}) + ftbot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) apiserver = ApiServer(ftbot) @@ -28,6 +36,21 @@ def botclient(default_conf, mocker): # Cleanup ... ? +def client_post(client, url, data={}): + headers = {"username": _TEST_USER, + "password": _TEST_PASS} + return client.post(url, + content_type="application/json", + data=data, + headers=headers) + + +def client_get(client, url): + headers = {"username": _TEST_USER, + "password": _TEST_PASS} + return client.get(url, headers=headers) + + def assert_response(response, expected_code=200): assert response.status_code == expected_code assert response.content_type == "application/json" @@ -36,7 +59,7 @@ def assert_response(response, expected_code=200): def test_api_not_found(botclient): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/invalid_url") + rc = client_post(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json == {"status": "error", "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", @@ -44,27 +67,57 @@ def test_api_not_found(botclient): } +def test_api_unauthorized(botclient): + ftbot, client = botclient + # Don't send user/pass information + rc = client.get(f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only username + ftbot.config['api_server']['username'] = "Ftrader" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only password + ftbot.config['api_server']['username'] = _TEST_USER + ftbot.config['api_server']['password'] = "WrongPassword" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + ftbot.config['api_server']['username'] = "Ftrader" + ftbot.config['api_server']['password'] = "WrongPassword" + + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + + + def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING - rc = client.post(f"{BASE_URI}/stop") + rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again - rc = client.post(f"{BASE_URI}/stop") + rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) assert rc.json == {'status': 'already stopped'} # Start bot - rc = client.post(f"{BASE_URI}/start") + rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again - rc = client.post(f"{BASE_URI}/start") + rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) assert rc.json == {'status': 'already running'} @@ -153,7 +206,7 @@ def test_api_cleanup(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/reload_conf") + rc = client_post(client, f"{BASE_URI}/reload_conf") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} assert ftbot.state == State.RELOAD_CONF @@ -163,7 +216,7 @@ def test_api_stopbuy(botclient): ftbot, client = botclient assert ftbot.config['max_open_trades'] != 0 - rc = client.post(f"{BASE_URI}/stopbuy") + rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} assert ftbot.config['max_open_trades'] == 0 @@ -193,7 +246,7 @@ def test_api_balance(botclient, mocker, rpc_balance): mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) - rc = client.get(f"{BASE_URI}/balance") + rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) assert "currencies" in rc.json assert len(rc.json["currencies"]) == 5 @@ -216,7 +269,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/count") + rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 0 @@ -224,7 +277,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): # Create some test data ftbot.create_trade() - rc = client.get(f"{BASE_URI}/count") + rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 assert rc.json["max"] == 1.0 @@ -240,7 +293,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/daily") + rc = client_get(client, f"{BASE_URI}/daily") assert_response(rc) assert len(rc.json) == 7 assert rc.json[0][0] == str(datetime.utcnow().date()) @@ -256,7 +309,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): get_fee=fee, markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/edge") + rc = client_get(client, f"{BASE_URI}/edge") assert_response(rc, 502) assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} @@ -272,7 +325,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 502) assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -282,7 +335,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 502) assert rc.json == {"error": "Error querying _profit: no closed trade"} @@ -291,7 +344,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li trade.close_date = datetime.utcnow() trade.is_open = False - rc = client.get(f"{BASE_URI}/profit") + rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) assert rc.json == {'avg_duration': '0:00:00', 'best_pair': 'ETH/BTC', @@ -344,7 +397,7 @@ def test_api_performance(botclient, mocker, ticker, fee): Trade.session.add(trade) Trade.session.flush() - rc = client.get(f"{BASE_URI}/performance") + rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json) == 2 assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, @@ -362,12 +415,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets) ) - rc = client.get(f"{BASE_URI}/status") + rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 502) assert rc.json == {'error': 'Error querying _status: no active trade'} ftbot.create_trade() - rc = client.get(f"{BASE_URI}/status") + rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 90.99181074, @@ -394,7 +447,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_version(botclient): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/version") + rc = client_get(client, f"{BASE_URI}/version") assert_response(rc) assert rc.json == {"version": __version__} @@ -402,15 +455,15 @@ def test_api_version(botclient): def test_api_blacklist(botclient, mocker): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/blacklist") + rc = client_get(client, f"{BASE_URI}/blacklist") assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, "method": "StaticPairList"} # Add ETH/BTC to blacklist - rc = client.post(f"{BASE_URI}/blacklist", data='{"blacklist": ["ETH/BTC"]}', - content_type='application/json') + rc = client_post(client, f"{BASE_URI}/blacklist", + data='{"blacklist": ["ETH/BTC"]}') assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], "length": 3, @@ -420,7 +473,7 @@ def test_api_blacklist(botclient, mocker): def test_api_whitelist(botclient): ftbot, client = botclient - rc = client.get(f"{BASE_URI}/whitelist") + rc = client_get(client, f"{BASE_URI}/whitelist") assert_response(rc) assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, @@ -430,7 +483,7 @@ def test_api_whitelist(botclient): def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient - rc = client.post(f"{BASE_URI}/forcebuy", content_type='application/json', + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} @@ -440,7 +493,7 @@ def test_api_forcebuy(botclient, mocker, fee): fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {"status": "Error buying pair ETH/BTC."} @@ -461,7 +514,7 @@ def test_api_forcebuy(botclient, mocker, fee): )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) - rc = client.post(f"{BASE_URI}/forcebuy", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, @@ -491,14 +544,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): ) patch_get_signal(ftbot, (True, False)) - rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} ftbot.create_trade() - rc = client.post(f"{BASE_URI}/forcesell", content_type="application/json", + rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} From 1fab884a2fcae07293a2d3139889d7dcb4462fe0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:14:09 +0200 Subject: [PATCH 079/134] use Authorization for client --- scripts/rest_client.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b31a1de50..ca3da737e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -26,10 +26,12 @@ logger = logging.getLogger("ft_rest_client") class FtRestClient(): - def __init__(self, serverurl): + def __init__(self, serverurl, username=None, password=None): self._serverurl = serverurl self._session = requests.Session() + self._username = username + self._password = password def _call(self, method, apipath, params: dict = None, data=None, files=None): @@ -41,6 +43,12 @@ class FtRestClient(): "Content-Type": "application/json" } + if self._username: + hd.update({"username": self._username}) + + if self._password: + hd.update({"password": self._password}) + # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string @@ -244,8 +252,11 @@ def main(args): config = load_config(args["config"]) url = config.get("api_server", {}).get("server_url", "127.0.0.1") port = config.get("api_server", {}).get("listen_port", "8080") + username = config.get("api_server", {}).get("username") + password = config.get("api_server", {}).get("password") + server_url = f"http://{url}:{port}" - client = FtRestClient(server_url) + client = FtRestClient(server_url, username, password) m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] command = args["command"] From 5bbd3c61581cac89741fdc65b8786078610afd75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:16:59 +0200 Subject: [PATCH 080/134] Add documentation --- docs/rest-api.md | 9 +++++++-- freqtrade/rpc/api_server.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 95eec3020..535163da4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -10,12 +10,17 @@ Sample configuration: "api_server": { "enabled": true, "listen_ip_address": "127.0.0.1", - "listen_port": 8080 + "listen_port": 8080, + "username": "Freqtrader", + "password": "SuperSecret1!" }, ``` !!! Danger: Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet, since others will potentially be able to control your bot. + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. + +!!! Danger: Password selection + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5e76e148c..d2001e91a 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -56,7 +56,7 @@ class ApiServer(RPC): def require_login(func): def func_wrapper(self, *args, **kwargs): - # Also works if no username/password is specified + # Also accepts empty username/password if it's missing in both config and request if (request.headers.get('username') == self._config['api_server'].get('username') and request.headers.get('password') == self._config['api_server'].get('password')): From 2da7145132956d4c0f02488e28133b9e35d16772 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:25:16 +0200 Subject: [PATCH 081/134] Switch auth to real basic auth --- freqtrade/rpc/api_server.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d2001e91a..14b15a3df 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -53,13 +53,16 @@ class ApiServer(RPC): 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): - # Also accepts empty username/password if it's missing in both config and request - if (request.headers.get('username') == self._config['api_server'].get('username') - and request.headers.get('password') == self._config['api_server'].get('password')): + auth = request.authorization + if auth and self.check_auth(auth.username, auth.password): return func(self, *args, **kwargs) else: return jsonify({"error": "Unauthorized"}), 401 From febcc3dddc043b568f9d067ca6ad1dc5c60bed7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:25:36 +0200 Subject: [PATCH 082/134] Adapt tests and rest_client to basic_auth --- freqtrade/tests/rpc/test_rpc_apiserver.py | 11 +++-------- scripts/rest_client.py | 13 ++----------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 620a7c34b..4c3aea89a 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -7,6 +7,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from flask import Flask +from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.persistence import Trade @@ -37,18 +38,14 @@ def botclient(default_conf, mocker): def client_post(client, url, data={}): - headers = {"username": _TEST_USER, - "password": _TEST_PASS} return client.post(url, content_type="application/json", data=data, - headers=headers) + headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) def client_get(client, url): - headers = {"username": _TEST_USER, - "password": _TEST_PASS} - return client.get(url, headers=headers) + return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) def assert_response(response, expected_code=200): @@ -95,8 +92,6 @@ def test_api_unauthorized(botclient): assert rc.json == {'error': 'Unauthorized'} - - def test_api_stop_workflow(botclient): ftbot, client = botclient assert ftbot.state == State.RUNNING diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ca3da737e..2261fba0b 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -30,8 +30,7 @@ class FtRestClient(): self._serverurl = serverurl self._session = requests.Session() - self._username = username - self._password = password + self._session.auth = (username, password) def _call(self, method, apipath, params: dict = None, data=None, files=None): @@ -43,12 +42,6 @@ class FtRestClient(): "Content-Type": "application/json" } - if self._username: - hd.update({"username": self._username}) - - if self._password: - hd.update({"password": self._password}) - # Split url schema, netloc, path, par, query, fragment = urlparse(basepath) # URLEncode query string @@ -57,9 +50,7 @@ class FtRestClient(): url = urlunparse((schema, netloc, path, par, query, fragment)) try: - resp = self._session.request(method, url, headers=hd, data=json.dumps(data), - # auth=self.session.auth - ) + resp = self._session.request(method, url, headers=hd, data=json.dumps(data)) # return resp.text return resp.json() except ConnectionError: From 90ece09ee98900b62e29562d4ccdd6f5d77d777e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:42:13 +0200 Subject: [PATCH 083/134] require username/password for API server --- freqtrade/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1b06eb726..4772952fc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -166,8 +166,10 @@ CONF_SCHEMA = { "minimum": 1024, "maximum": 65535 }, + 'username': {'type': 'string'}, + 'password': {'type': 'string'}, }, - 'required': ['enabled', 'listen_ip_address', 'listen_port'] + 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, From b6484cb2b42e9f7df977f5717180e2442ce42e5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:54:35 +0200 Subject: [PATCH 084/134] Replace technical link --- Dockerfile.technical | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.technical b/Dockerfile.technical index 5339eb232..9431e72d0 100644 --- a/Dockerfile.technical +++ b/Dockerfile.technical @@ -3,4 +3,4 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/berlinguyinca/technical + && pip install git+https://github.com/freqtrade/technical From 4394701de37853183b84d9d982745d29f8fe8a77 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:13:18 +0200 Subject: [PATCH 085/134] Seperate docker-documentation --- README.md | 1 - docs/docker.md | 204 +++++++++++++++++++++++++++++++++++++++++ docs/index.md | 7 +- docs/installation.md | 210 ++++--------------------------------------- 4 files changed, 226 insertions(+), 196 deletions(-) create mode 100644 docs/docker.md diff --git a/README.md b/README.md index 8f7578561..98dad1d2e 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ The project is currently setup in two main branches: - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - ## A note on Binance For Binance, please add `"BNB/"` to your blacklist to avoid issues. diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 000000000..767cabf01 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,204 @@ +# Using FreqTrade with Docker + +## Install Docker + +Start by downloading and installing Docker CE for your platform: + +* [Mac](https://docs.docker.com/docker-for-mac/install/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) +* [Linux](https://docs.docker.com/install/) + +Once you have Docker installed, simply create the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. + +## Download the official FreqTrade docker image + +Pull the image from docker hub. + +Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +```bash +docker pull freqtradeorg/freqtrade:develop +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:develop freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +Production + +```bash +touch tradesv3.sqlite +```` + +Dry-Run + +```bash +touch tradesv3.dryrun.sqlite +``` + +!!! Note + Make sure to use the path to this file when starting the bot in docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libaries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Note + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +Should you find this irritating please add the following to your docker commands: + +##### Linux + +``` bash +-v /etc/timezone:/etc/timezone:ro + +# Complete command: +docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +##### MacOS + +There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. + +```bash +docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. + To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` + +!!! Note + All available command line arguments can be added to the end of the `docker run` command. + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade --strategy AwsomelyProfitableStrategy backtesting +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/index.md b/docs/index.md index 9abc71747..a6ae6312d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,12 +36,14 @@ Freqtrade is a cryptocurrency trading bot written in Python. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### Up to date clock + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -58,10 +61,12 @@ To run this bot we recommend you a cloud instance with a minimum of: ## Support + Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. ## Ready to try? + Begin by reading our installation guide [here](installation). diff --git a/docs/installation.md b/docs/installation.md index 11ddc010d..ed38c1340 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,7 +1,9 @@ # Installation + This page explains how to prepare your environment for running the bot. ## Prerequisite + Before running your bot in production you will need to setup few external API. In production mode, the bot required valid Bittrex API credentials and a Telegram bot (optional but recommended). @@ -10,9 +12,11 @@ credentials and a Telegram bot (optional but recommended). - [Backtesting commands](#setup-your-telegram-bot) ### Setup your exchange account + *To be completed, please feel free to complete this section.* ### Setup your Telegram bot + The only things you need is a working Telegram bot and its API token. Below we explain how to create your Telegram Bot, and how to get your Telegram user id. @@ -35,7 +39,9 @@ Good. Now let's choose a username for your bot. It must end in `bot`. Like this, **1.5. Father bot will return you the token (API key)**
Copy it and keep it you will use it for the config parameter `token`. + *BotFather response:* + ```hl_lines="4" Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. @@ -44,15 +50,18 @@ Use this token to access the HTTP API: For a description of the Bot API, see this page: https://core.telegram.org/bots/api ``` + **1.6. Don't forget to start the conversation with your bot, by clicking /START button** ### 2. Get your user id + **2.1. Talk to https://telegram.me/userinfobot** **2.2. Get your "Id", you will use it for the config parameter `chat_id`.** -
+ ## Quick start + Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. ```bash @@ -61,9 +70,10 @@ cd freqtrade git checkout develop ./setup.sh --install ``` + !!! Note Windows installation is explained [here](#windows). -
+ ## Easy Installation - Linux Script If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. @@ -101,193 +111,6 @@ Config parameter is a `config.json` configurator. This script will ask you quest ------ -## Automatic Installation - Docker - -Start by downloading Docker for your platform: - -* [Mac](https://www.docker.com/products/docker#/mac) -* [Windows](https://www.docker.com/products/docker#/windows) -* [Linux](https://www.docker.com/products/docker#/linux) - -Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo. - -### 1. Prepare the Bot - -**1.1. Clone the git repository** - -Linux/Mac/Windows with WSL -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -**1.2. (Optional) Checkout the develop branch** - -```bash -git checkout develop -``` - -**1.3. Go into the new directory** - -```bash -cd freqtrade -``` - -**1.4. Copy `config.json.example` to `config.json`** - -```bash -cp -n config.json.example config.json -``` - -> To edit the config please refer to the [Bot Configuration](configuration.md) page. - -**1.5. Create your database file *(optional - the bot will create it if it is missing)** - -Production - -```bash -touch tradesv3.sqlite -```` - -Dry-Run - -```bash -touch tradesv3.dryrun.sqlite -``` - -### 2. Download or build the docker image - -Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used. - -Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -**2.1. Download the docker image** - -Pull the image from docker hub and (optionally) change the name of the image - -```bash -docker pull freqtradeorg/freqtrade:develop -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:develop freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -**2.2. Build the Docker image** - -```bash -cd freqtrade -docker build -t freqtrade . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f ./Dockerfile.develop -t freqtrade-dev . -``` - -For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. - -### 3. Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -### 4. Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. - -```bash -docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. - -### 5. Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -**5.1. Move your config file and database** - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -**5.2. Run the docker image** - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. - To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` - -!!! Note - All command line arguments can be added to the end of the `docker run` command. - -### 6. Monitor your Docker instance - -You can then use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### 7. Backtest with docker - -The following assumes that the above steps (1-4) have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade --strategy AwsomelyProfitableStrategy backtesting -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional parameters can be appended after the image name (`freqtrade` in the above example). - ------- - ## Custom Installation We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. @@ -413,7 +236,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run python3.6 freqtrade -c config.json ``` -*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. +*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. #### 7. [Optional] Configure `freqtrade` as a `systemd` service @@ -441,14 +264,13 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co as the watchdog. !!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a - Docker container. + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ ## Windows -We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure). +We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. If that is not available on your system, feel free to try the instructions below, which led to success for some. @@ -492,7 +314,7 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. --- From 3e0a71f69f99ef82ebc6ad4730259ed4e780d106 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:27:18 +0200 Subject: [PATCH 086/134] Add docker install script to mkdocs index --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 9932ff316..489107f2e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ site_name: Freqtrade nav: - About: index.md - Installation: installation.md + - Installation Docker: docker.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md From 26a8cdcc031d6fa7cba4dc2a3f7ada3807d5a297 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:27:36 +0200 Subject: [PATCH 087/134] Move telegram-setup to telegram page --- docs/installation.md | 52 +++--------------------------------------- docs/telegram-usage.md | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index ed38c1340..d215dc8d6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,60 +5,14 @@ This page explains how to prepare your environment for running the bot. ## Prerequisite Before running your bot in production you will need to setup few -external API. In production mode, the bot required valid Bittrex API -credentials and a Telegram bot (optional but recommended). +external API. In production mode, the bot will require valid Exchange API +credentials. We also reccomend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - [Setup your exchange account](#setup-your-exchange-account) -- [Backtesting commands](#setup-your-telegram-bot) ### Setup your exchange account -*To be completed, please feel free to complete this section.* - -### Setup your Telegram bot - -The only things you need is a working Telegram bot and its API token. -Below we explain how to create your Telegram Bot, and how to get your -Telegram user id. - -### 1. Create your Telegram bot - -**1.1. Start a chat with https://telegram.me/BotFather** - -**1.2. Send the message `/newbot`. ** *BotFather response:* -``` -Alright, a new bot. How are we going to call it? Please choose a name for your bot. -``` - -**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)** -*BotFather response:* -``` -Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. -``` -**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")** - -**1.5. Father bot will return you the token (API key)**
-Copy it and keep it you will use it for the config parameter `token`. - -*BotFather response:* - -```hl_lines="4" -Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. - -Use this token to access the HTTP API: -521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0 - -For a description of the Bot API, see this page: https://core.telegram.org/bots/api -``` - -**1.6. Don't forget to start the conversation with your bot, by clicking /START button** - -### 2. Get your user id - -**2.1. Talk to https://telegram.me/userinfobot** - -**2.2. Get your "Id", you will use it for the config parameter -`chat_id`.** +You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. ## Quick start diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 3947168c5..e06d4fdfc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -1,10 +1,45 @@ # Telegram usage -## Prerequisite +## Setup your Telegram bot -To control your bot with Telegram, you need first to -[set up a Telegram bot](installation.md) -and add your Telegram API keys into your config file. +Below we explain how to create your Telegram Bot, and how to get your +Telegram user id. + +### 1. Create your Telegram bot + +Start a chat with the [Telegram BotFather](https://telegram.me/BotFather) + +Send the message `/newbot`. + +*BotFather response:* + +> Alright, a new bot. How are we going to call it? Please choose a name for your bot. + +Choose the public name of your bot (e.x. `Freqtrade bot`) + +*BotFather response:* + +> Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. + +Choose the name id of your bot and send it to the BotFather (e.g. "`My_own_freqtrade_bot`") + +*BotFather response:* + +> Done! Congratulations on your new bot. You will find it at `t.me/yourbots_name_bot`. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. + +> Use this token to access the HTTP API: `22222222:APITOKEN` + +> For a description of the Bot API, see this page: https://core.telegram.org/bots/api Father bot will return you the token (API key) + +Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it for the config parameter `token`. + +Don't forget to start the conversation with your bot, by clicking `/START` button + +### 2. Get your user id + +Talk to the [userinfobot](https://telegram.me/userinfobot) + +Get your "Id", you will use it for the config parameter `chat_id`. ## Telegram commands From 9225cdea8a770453c59b203fb8c2b8f8fbb40fe2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:51:52 +0200 Subject: [PATCH 088/134] Move validate_backtest_data and get_timeframe to histoyr --- freqtrade/data/history.py | 44 ++++++++++++++++++++++++++++--- freqtrade/edge/__init__.py | 4 +-- freqtrade/optimize/__init__.py | 48 ---------------------------------- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3bec63926..e0f9f67db 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -5,19 +5,21 @@ Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ + import logging +import operator +from datetime import datetime from pathlib import Path -from typing import Optional, List, Dict, Tuple, Any +from typing import Any, Dict, List, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade import misc, OperationalException +from freqtrade import OperationalException, misc from freqtrade.arguments import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange, timeframe_to_minutes - logger = logging.getLogger(__name__) @@ -243,3 +245,39 @@ def download_pair_history(datadir: Optional[Path], f'Error: {e}' ) return False + + +def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: + """ + Get the maximum timeframe for the given backtest data + :param data: dictionary with preprocessed backtesting data + :return: tuple containing min_date, max_date + """ + timeframe = [ + (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + for frame in data.values() + ] + return min(timeframe, key=operator.itemgetter(0))[0], \ + max(timeframe, key=operator.itemgetter(1))[1] + + +def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, + max_date: datetime, ticker_interval_mins: int) -> bool: + """ + Validates preprocessed backtesting data for missing values and shows warnings about it that. + + :param data: dictionary with preprocessed backtesting data + :param min_date: start-date of the data + :param max_date: end-date of the data + :param ticker_interval_mins: ticker interval in minutes + """ + # total difference in minutes / interval-minutes + expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + found_missing = False + for pair, df in data.items(): + dflen = len(df) + if dflen < expected_frames: + found_missing = True + logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", + pair, expected_frames, dflen, expected_frames - dflen) + return found_missing diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 053be6bc3..3ddff4772 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -13,7 +13,6 @@ from freqtrade import constants, OperationalException from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.data import history -from freqtrade.optimize import get_timeframe from freqtrade.strategy.interface import SellType @@ -49,7 +48,6 @@ class Edge(): self.strategy = strategy self.ticker_interval = self.strategy.ticker_interval self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.get_timeframe = get_timeframe self.advise_sell = self.strategy.advise_sell self.advise_buy = self.strategy.advise_buy @@ -117,7 +115,7 @@ class Edge(): preprocessed = self.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) + min_date, max_date = history.get_timeframe(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 19b8dd90a..f4f31720b 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1,49 +1 @@ -# pragma pylint: disable=missing-docstring - -import logging -from datetime import datetime -from typing import Dict, Tuple -import operator - -import arrow -from pandas import DataFrame - from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401 - -logger = logging.getLogger(__name__) - - -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: - """ - Get the maximum timeframe for the given backtest data - :param data: dictionary with preprocessed backtesting data - :return: tuple containing min_date, max_date - """ - timeframe = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) - for frame in data.values() - ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] - - -def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, - max_date: datetime, ticker_interval_mins: int) -> bool: - """ - Validates preprocessed backtesting data for missing values and shows warnings about it that. - - :param data: dictionary with preprocessed backtesting data - :param min_date: start-date of the data - :param max_date: end-date of the data - :param ticker_interval_mins: ticker interval in minutes - """ - # total difference in minutes / interval-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) - found_missing = False - for pair, df in data.items(): - dflen = len(df) - if dflen < expected_frames: - found_missing = True - logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", - pair, expected_frames, dflen, expected_frames - dflen) - return found_missing From b38c43141c44d26702cb69f88ce58b85c111e577 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 16:53:35 +0200 Subject: [PATCH 089/134] Adjust imports to new location --- freqtrade/optimize/backtesting.py | 7 +++---- freqtrade/optimize/hyperopt.py | 3 +-- freqtrade/tests/data/test_converter.py | 3 +-- freqtrade/tests/optimize/test_backtest_detail.py | 12 ++++++------ freqtrade/tests/optimize/test_backtesting.py | 6 +++--- freqtrade/tests/optimize/test_hyperopt.py | 2 +- freqtrade/tests/optimize/test_optimize.py | 15 +++++++-------- 7 files changed, 22 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 51122cfb2..c7ce29120 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,6 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import optimize from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -440,10 +439,10 @@ class Backtesting(object): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called) - optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) + history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes(self.ticker_interval)) logger.info( 'Backtesting with data from %s up to %s (%s days)..', min_date.isoformat(), diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 92589aed2..68c7b2508 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,9 +23,8 @@ from skopt.space import Dimension from freqtrade import DependencyException from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.data.history import load_data +from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data from freqtrade.exchange import timeframe_to_minutes -from freqtrade.optimize import get_timeframe, validate_backtest_data from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.resolvers import HyperOptResolver diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 46d564003..4c8de575d 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -2,8 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history -from freqtrade.optimize import validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe from freqtrade.tests.conftest import log_has diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index b98369533..32c6bd09b 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -2,17 +2,17 @@ import logging from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame - -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType -from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe, - _get_frame_time_from_offset, tests_ticker_interval) from freqtrade.tests.conftest import patch_exchange - +from freqtrade.tests.optimize import (BTContainer, BTrade, + _build_backtest_dataframe, + _get_frame_time_from_offset, + tests_ticker_interval) # Test 1 Minus 8% Close # Test with Stop-loss at 1% diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 6a39deed4..07ad7eaff 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) from freqtrade.state import RunMode @@ -472,7 +472,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -507,7 +507,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index f50f58e5b..9d1789171 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -12,7 +12,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start -from freqtrade.resolvers import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index d746aa44f..401592b53 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade import optimize from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.exchange import timeframe_to_minutes @@ -18,7 +17,7 @@ def test_get_timeframe(default_conf, mocker) -> None: pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -35,10 +34,10 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: fill_up_missing=False ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) caplog.clear() - assert optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) + assert history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -59,8 +58,8 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: ) ) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) caplog.clear() - assert not optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) + assert not history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 From 236c392d282fc1516f05531651546c732b520b84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:00:31 +0200 Subject: [PATCH 090/134] Don't load hyperopts / optimize dependency tree if that module is not used --- freqtrade/arguments.py | 6 +- freqtrade/optimize/__init__.py | 100 +++++++++++++++++++++++++++++- freqtrade/optimize/backtesting.py | 42 +------------ freqtrade/optimize/hyperopt.py | 66 +------------------- freqtrade/resolvers/__init__.py | 3 +- 5 files changed, 106 insertions(+), 111 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 5afa8fa06..e79d0c6d4 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,13 +340,13 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import backtesting, hyperopt, edge_cli + from freqtrade.optimize import start_backtesting, start_hyperopt, edge_cli subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') - backtesting_cmd.set_defaults(func=backtesting.start) + backtesting_cmd.set_defaults(func=start_backtesting) self.optimizer_shared_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) @@ -358,7 +358,7 @@ class Arguments(object): # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') - hyperopt_cmd.set_defaults(func=hyperopt.start) + hyperopt_cmd.set_defaults(func=start_hyperopt) self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index f4f31720b..34076ee43 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1 +1,99 @@ -from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401 +import logging +from argparse import Namespace +from typing import Any, Dict + +from filelock import FileLock, Timeout + +from freqtrade import DependencyException, constants +from freqtrade.configuration import Configuration +from freqtrade.state import RunMode + +logger = logging.getLogger(__name__) + + +def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: + """ + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration + """ + configuration = Configuration(args, method) + config = configuration.load_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + if method == RunMode.BACKTEST: + if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: + raise DependencyException('stake amount could not be "%s" for backtesting' % + constants.UNLIMITED_STAKE_AMOUNT) + + if method == RunMode.HYPEROPT: + # Special cases for Hyperopt + if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': + logger.error("Please don't use --strategy for hyperopt.") + logger.error( + "Read the documentation at " + "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " + "to understand how to configure hyperopt.") + raise DependencyException("--strategy configured but not supported for hyperopt") + + return config + + +def start_backtesting(args: Namespace) -> None: + """ + Start Backtesting script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading backtesting module when it's not used + from freqtrade.optimize.backtesting import Backtesting + + # Initialize configuration + config = setup_configuration(args, RunMode.BACKTEST) + + logger.info('Starting freqtrade in Backtesting mode') + + # Initialize backtesting object + backtesting = Backtesting(config) + backtesting.start() + + +def start_hyperopt(args: Namespace) -> None: + """ + Start hyperopt script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading hyperopt module when it's not used + from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE + + # Initialize configuration + config = setup_configuration(args, RunMode.HYPEROPT) + + logger.info('Starting freqtrade in Hyperopt mode') + + lock = FileLock(HYPEROPT_LOCKFILE) + + try: + with lock.acquire(timeout=1): + + # Remove noisy log messages + logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + logging.getLogger('filelock').setLevel(logging.WARNING) + + # Initialize backtesting object + hyperopt = Hyperopt(config) + hyperopt.start() + + except Timeout: + logger.info("Another running instance of freqtrade Hyperopt detected.") + logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " + "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " + "or on separate machines.") + logger.info("Quitting now.") + # TODO: return False here in order to help freqtrade to exit + # with non-zero exit code... + # Same in Edge and Backtesting start() functions. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c7ce29120..bdd42943b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,7 +4,6 @@ This module contains the backtesting logic """ import logging -from argparse import Namespace from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path @@ -13,9 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes @@ -23,8 +20,7 @@ from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType, IStrategy - +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -485,39 +481,3 @@ class Backtesting(object): print(' Strategy Summary '.center(133, '=')) print(self._generate_text_table_strategy(all_results)) print('\nFor more details, please look at the detail tables above') - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.BACKTEST) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: - raise DependencyException('stake amount could not be "%s" for backtesting' % - constants.UNLIMITED_STAKE_AMOUNT) - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Backtesting mode') - - # Initialize backtesting object - backtesting = Backtesting(config) - backtesting.start() diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 68c7b2508..d19d54031 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,27 +7,22 @@ This module contains the hyperopt logic import logging import os import sys -from argparse import Namespace from math import exp from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List -from filelock import Timeout, FileLock from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade import DependencyException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data from freqtrade.exchange import timeframe_to_minutes from freqtrade.optimize.backtesting import Backtesting -from freqtrade.state import RunMode -from freqtrade.resolvers import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver logger = logging.getLogger(__name__) @@ -342,62 +337,3 @@ class Hyperopt(Backtesting): self.save_trials() self.log_trials_result() - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the Hyperopt module - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.HYPEROPT) - config = configuration.load_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': - logger.error("Please don't use --strategy for hyperopt.") - logger.error( - "Read the documentation at " - "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " - "to understand how to configure hyperopt.") - raise DependencyException("--strategy configured but not supported for hyperopt") - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Hyperopt mode') - - lock = FileLock(HYPEROPT_LOCKFILE) - - try: - with lock.acquire(timeout=1): - - # Remove noisy log messages - logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) - logging.getLogger('filelock').setLevel(logging.WARNING) - - # Initialize backtesting object - hyperopt = Hyperopt(config) - hyperopt.start() - - except Timeout: - logger.info("Another running instance of freqtrade Hyperopt detected.") - logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " - "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " - "or on separate machines.") - logger.info("Quitting now.") - # TODO: return False here in order to help freqtrade to exit - # with non-zero exit code... - # Same in Edge and Backtesting start() functions. diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 5cf6c616a..8f79349fe 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,5 +1,6 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 +# Don't import HyperoptResolver to avoid loading the whole Optimize tree +# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 From 65a4862d1f420b9a1df673ba6bfe32a6457db639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:01:43 +0200 Subject: [PATCH 091/134] Adapt tests to load start_* methods from optimize --- freqtrade/tests/optimize/test_backtesting.py | 16 ++++++++-------- freqtrade/tests/optimize/test_hyperopt.py | 13 +++++++------ freqtrade/tests/test_main.py | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 07ad7eaff..5b42cae34 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -18,8 +18,8 @@ from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timeframe -from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, - start) +from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType @@ -178,7 +178,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'backtesting' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -228,7 +228,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--export-filename', 'foo_bar.json' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -290,7 +290,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog ] with pytest.raises(DependencyException, match=r'.*stake amount.*'): - setup_configuration(get_args(args)) + setup_configuration(get_args(args), RunMode.BACKTEST) def test_start(mocker, fee, default_conf, caplog) -> None: @@ -307,7 +307,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: 'backtesting' ] args = get_args(args) - start(args) + start_backtesting(args) assert log_has( 'Starting freqtrade in Backtesting mode', caplog.record_tuples @@ -847,7 +847,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--disable-max-market-positions' ] args = get_args(args) - start(args) + start_backtesting(args) # check the logs, that will contain the backtest result exists = [ 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', @@ -901,7 +901,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'TestStrategy', ] args = get_args(args) - start(args) + start_backtesting(args) # 2 backtests, 4 tables assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9d1789171..9128efa0c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -11,7 +11,8 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start +from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -52,7 +53,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca 'hyperopt' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -100,7 +101,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--print-all' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -183,7 +184,7 @@ def test_start(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -214,7 +215,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -239,7 +240,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ] args = get_args(args) with pytest.raises(DependencyException): - start(args) + start_hyperopt(args) assert log_has( "Please don't use --strategy for hyperopt.", caplog.record_tuples diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e4ffc5fae..1292fd24d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -19,7 +19,7 @@ def test_parse_args_backtesting(mocker) -> None: Test that main() can start backtesting and also ensure we can pass some specific arguments further argument parsing is done in test_arguments.py """ - backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) + backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] @@ -32,7 +32,7 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: - hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) + hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] From 104f1212e6134973ff89b00f042552c389499bb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:05:19 +0200 Subject: [PATCH 092/134] Move edge_cli_start to optimize --- freqtrade/arguments.py | 4 ++-- freqtrade/optimize/__init__.py | 16 ++++++++++++++++ freqtrade/optimize/edge_cli.py | 34 ---------------------------------- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index e79d0c6d4..d6f0063d0 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,7 +340,7 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import start_backtesting, start_hyperopt, edge_cli + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edgecli subparsers = self.parser.add_subparsers(dest='subparser') @@ -352,7 +352,7 @@ class Arguments(object): # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=edge_cli.start) + edge_cmd.set_defaults(func=start_edgecli) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 34076ee43..cb01950b4 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -97,3 +97,19 @@ def start_hyperopt(args: Namespace) -> None: # TODO: return False here in order to help freqtrade to exit # with non-zero exit code... # Same in Edge and Backtesting start() functions. + + +def start_edgecli(args: Namespace) -> None: + """ + Start Edge script + :param args: Cli args from Arguments() + :return: None + """ + from freqtrade.optimize.edge_cli import EdgeCli + # Initialize configuration + config = setup_configuration(args, RunMode.EDGECLI) + logger.info('Starting freqtrade in Edge mode') + + # Initialize Edge object + edge_cli = EdgeCli(config) + edge_cli.start() diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index d37b930b8..8232c79c9 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,16 +4,13 @@ This module contains the edge backtesting interface """ import logging -from argparse import Namespace from typing import Dict, Any from tabulate import tabulate from freqtrade.edge import Edge from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -77,34 +74,3 @@ class EdgeCli(object): if result: print('') # blank line for readability print(self._generate_edge_table(self.edge._cached_pairs)) - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for edge backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.EDGECLI) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - return config - - -def start(args: Namespace) -> None: - """ - Start Edge script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - logger.info('Starting freqtrade in Edge mode') - - # Initialize Edge object - edge_cli = EdgeCli(config) - edge_cli.start() From 8ad30e262578f076c7b9bf8f2399fdbcadc1aa06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:06:18 +0200 Subject: [PATCH 093/134] Adapt tests --- freqtrade/tests/optimize/test_edge_cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 488d552c8..dc40cc85c 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,7 +7,8 @@ from unittest.mock import MagicMock from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize.edge_cli import EdgeCli, setup_configuration, start +from freqtrade.optimize import start_edgecli, setup_configuration +from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -27,7 +28,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.EDGECLI) assert config['runmode'] == RunMode.EDGECLI assert 'max_open_trades' in config @@ -67,7 +68,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.EDGECLI) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -106,7 +107,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: 'edge' ] args = get_args(args) - start(args) + start_edgecli(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples From 71447e55aac787dce881adf32da776c747838791 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:14:31 +0200 Subject: [PATCH 094/134] Update missing import --- scripts/plot_dataframe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7fdc607e0..8a87d971c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -41,9 +41,10 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data from freqtrade.exchange import Exchange -from freqtrade.optimize.backtesting import setup_configuration +from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} @@ -107,7 +108,7 @@ def get_trading_env(args: Namespace): global _CONF # Load the configuration - _CONF.update(setup_configuration(args)) + _CONF.update(setup_configuration(args, RunMode.BACKTEST)) print(_CONF) pairs = args.pairs.split(',') From 201e02e73fbb39da0ee2b9a0687339edc489b49a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 20:25:59 +0200 Subject: [PATCH 095/134] Add test for Timeout - move tests to test_history --- freqtrade/tests/data/test_history.py | 70 +++++++++++++++++++++-- freqtrade/tests/optimize/test_hyperopt.py | 25 +++++++- freqtrade/tests/optimize/test_optimize.py | 65 --------------------- 3 files changed, 89 insertions(+), 71 deletions(-) delete mode 100644 freqtrade/tests/optimize/test_optimize.py diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 0d4210d3a..4d70d4cdd 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -2,24 +2,25 @@ import json import os -from pathlib import Path import uuid +from pathlib import Path from shutil import copyfile import arrow -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade import OperationalException from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, - load_tickerdata_file, - make_testdata_path, + load_tickerdata_file, make_testdata_path, trim_tickerlist) +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json -from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import get_patched_exchange, log_has, patch_exchange # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -495,3 +496,62 @@ def test_file_dump_json_tofile() -> None: # Remove the file _clean_test_file(file) + + +def test_get_timeframe(default_conf, mocker) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + ) + ) + min_date, max_date = history.get_timeframe(data) + assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + + +def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'], + fill_up_missing=False + ) + ) + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('1m')) + assert len(caplog.record_tuples) == 1 + assert log_has( + "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + caplog.record_tuples) + + +def test_validate_backtest_data(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + timerange = TimeRange('index', 'index', 200, 250) + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='5m', + pairs=['UNITTEST/BTC'], + timerange=timerange + ) + ) + + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert not history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('5m')) + assert len(caplog.record_tuples) == 0 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9128efa0c..b41f8ac36 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -3,6 +3,7 @@ import json import os from datetime import datetime from unittest.mock import MagicMock +from filelock import Timeout import pandas as pd import pytest @@ -11,7 +12,7 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -247,6 +248,28 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ) +def test_start_filelock(mocker, default_conf, caplog) -> None: + start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) + mocker.patch( + 'freqtrade.configuration.Configuration._load_config_file', + lambda *args, **kwargs: default_conf + ) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) + patch_exchange(mocker) + + args = [ + '--config', 'config.json', + 'hyperopt', + '--epochs', '5' + ] + args = get_args(args) + start_hyperopt(args) + assert log_has( + "Another running instance of freqtrade Hyperopt detected.", + caplog.record_tuples + ) + + def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py deleted file mode 100644 index 401592b53..000000000 --- a/freqtrade/tests/optimize/test_optimize.py +++ /dev/null @@ -1,65 +0,0 @@ -# pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade.arguments import TimeRange -from freqtrade.data import history -from freqtrade.exchange import timeframe_to_minutes -from freqtrade.strategy.default_strategy import DefaultStrategy -from freqtrade.tests.conftest import log_has, patch_exchange - - -def test_get_timeframe(default_conf, mocker) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'] - ) - ) - min_date, max_date = history.get_timeframe(data) - assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' - - -def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'], - fill_up_missing=False - ) - ) - min_date, max_date = history.get_timeframe(data) - caplog.clear() - assert history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) - assert len(caplog.record_tuples) == 1 - assert log_has( - "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", - caplog.record_tuples) - - -def test_validate_backtest_data(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - timerange = TimeRange('index', 'index', 200, 250) - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='5m', - pairs=['UNITTEST/BTC'], - timerange=timerange - ) - ) - - min_date, max_date = history.get_timeframe(data) - caplog.clear() - assert not history.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) - assert len(caplog.record_tuples) == 0 From 0e228acbfb5f2605c099696e37915e8d8a8fe005 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 25 May 2019 22:42:17 +0300 Subject: [PATCH 096/134] minor: exchange debug logging humanized --- freqtrade/exchange/exchange.py | 19 +++++++++++++++---- freqtrade/tests/exchange/test_exchange.py | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 66857a7a5..72a0efb1f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -510,7 +510,11 @@ class Exchange(object): _LIMIT = 500 one_call = timeframe_to_msecs(ticker_interval) * _LIMIT - logger.debug("one_call: %s msecs", one_call) + logger.debug( + "one_call: %s msecs (%s)", + one_call, + arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) + ) input_coroutines = [self._async_get_candle_history( pair, ticker_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] @@ -541,7 +545,10 @@ class Exchange(object): or self._now_is_time_to_refresh(pair, ticker_interval)): input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) else: - logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval) + logger.debug( + "Using cached ohlcv data for pair %s, interval %s ...", + pair, ticker_interval + ) tickers = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) @@ -578,7 +585,11 @@ class Exchange(object): """ try: # fetch ohlcv asynchronously - logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms) + s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' + logger.debug( + "Fetching pair %s, interval %s, since %s %s...", + pair, ticker_interval, since_ms, s + ) data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, since=since_ms) @@ -593,7 +604,7 @@ class Exchange(object): except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) return pair, ticker_interval, [] - logger.debug("done fetching %s, %s ...", pair, ticker_interval) + logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) return pair, ticker_interval, data except ccxt.NotSupported as e: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 924ed538f..fda9c8241 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1016,7 +1016,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert log_has(f"Using cached ohlcv data for {pairs[0][0]}, {pairs[0][1]} ...", + assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", caplog.record_tuples) From e335e6c480350e8b7c91939cfae99e5ec9c91170 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 13:40:07 +0200 Subject: [PATCH 097/134] Fix some wordings --- docs/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 767cabf01..939ab3f7d 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -8,7 +8,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -Once you have Docker installed, simply create the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. +Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. ## Download the official FreqTrade docker image @@ -74,7 +74,7 @@ touch tradesv3.dryrun.sqlite Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. -To add additional libaries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. ```bash docker build -t freqtrade -f Dockerfile.technical . @@ -164,7 +164,7 @@ docker run -d \ To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` !!! Note - All available command line arguments can be added to the end of the `docker run` command. + All available bot command line parameters can be added to the end of the `docker run` command. ### Monitor your Docker instance @@ -201,4 +201,4 @@ docker run -d \ Head over to the [Backtesting Documentation](backtesting.md) for more details. !!! Note - Additional parameters can be appended after the image name (`freqtrade` in the above example). + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). From dab4307e04dd3ccbe3f860113a4ace43463d4fb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 14:40:03 +0200 Subject: [PATCH 098/134] Add secure way to genreate password, warn if no password is defined --- docs/rest-api.md | 7 +++++++ freqtrade/rpc/api_server.py | 4 ++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 10 +++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 535163da4..0508f83e4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -24,6 +24,13 @@ Sample configuration: You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. +To generate a secure password, either use a password manager, or use the below code snipped. + +``` python +import secrets +secrets.token_hex() +``` + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 14b15a3df..711202b27 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -106,6 +106,10 @@ class ApiServer(RPC): logger.warning("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json") + if not self._config['api_server'].get('password'): + logger.warning("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!") + # Run the Server logger.info('Starting Local Rest Server.') try: diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py index 4c3aea89a..b7721fd8e 100644 --- a/freqtrade/tests/rpc/test_rpc_apiserver.py +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -156,7 +156,9 @@ def test_api_run(default_conf, mocker, caplog): server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", - "listen_port": "8089"}}) + "listen_port": "8089", + "password": "", + }}) apiserver.run() assert server_mock.call_count == 1 @@ -170,13 +172,15 @@ def test_api_run(default_conf, mocker, caplog): assert log_has("SECURITY WARNING - This is insecure please set to your loopback," "e.g 127.0.0.1 in config.json", caplog.record_tuples) + assert log_has("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!", + caplog.record_tuples) # Test crashing flask caplog.clear() mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) apiserver.run() - assert log_has("Api server failed to start.", - caplog.record_tuples) + assert log_has("Api server failed to start.", caplog.record_tuples) def test_api_cleanup(default_conf, mocker, caplog): From 1988662607842601b7d1813c074a07bcf9b48a13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2019 20:19:06 +0200 Subject: [PATCH 099/134] Update plot-script to work with exported trades --- scripts/plot_dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7fdc607e0..cc54bfff2 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -74,7 +74,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram file = Path(args.exportfilename) if file.exists(): - load_backtest_data(file) + trades = load_backtest_data(file) else: trades = pd.DataFrame([], columns=BT_DATA_COLUMNS) From 196a1bcc267696379efb539136a37f29399c92d9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:06 +0000 Subject: [PATCH 100/134] Update ccxt from 1.18.551 to 1.18.578 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3f755b8c0..1defdf19e 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.551 +ccxt==1.18.578 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 From bfb6dc4a8ea4eb031ef554c0b299fc4b9d380327 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:07 +0000 Subject: [PATCH 101/134] Update cachetools from 3.1.0 to 3.1.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 1defdf19e..e885e3e0f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.18.578 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 -cachetools==3.1.0 +cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 From 09e037c96ec2a0b1edaefef1f3641f0b7e8b6bc5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 27 May 2019 15:29:09 +0000 Subject: [PATCH 102/134] Update scikit-learn from 0.21.1 to 0.21.2 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e885e3e0f..b149abacd 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -8,7 +8,7 @@ cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 -scikit-learn==0.21.1 +scikit-learn==0.21.2 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 From f7766d305b73d01ee90d7f45bfca5bbee30dfefd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 May 2019 19:42:12 +0200 Subject: [PATCH 103/134] Improve plotting documentation --- docs/plotting.md | 55 +++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 60c642ab3..20183ab9c 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,63 +1,79 @@ # Plotting + This page explains how to plot prices, indicator, profits. ## Installation Plotting scripts use Plotly library. Install/upgrade it with: -``` -pip install --upgrade plotly +``` bash +pip install -U -r requirements-plot.txt ``` At least version 2.3.0 is required. ## Plot price and indicators + Usage for the price plotter: -``` -script/plot_dataframe.py [-h] [-p pairs] [--live] +``` bash +python3 script/plot_dataframe.py [-h] [-p pairs] [--live] ``` Example -``` -python scripts/plot_dataframe.py -p BTC/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH ``` The `-p` pairs argument, can be used to specify pairs you would like to plot. -**Advanced use** +### Advanced use To plot multiple pairs, separate them with a comma: -``` -python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH ``` To plot the current live price use the `--live` flag: -``` -python scripts/plot_dataframe.py -p BTC/ETH --live + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --live ``` To plot a timerange (to zoom in): + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 ``` -python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 -``` + Timerange doesn't work with live data. To plot trades stored in a database use `--db-url` argument: + +``` bash +python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH + +To polt trades from a backtesting result, use `--export-filename ` + +``` bash +python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH ``` To plot a test strategy the strategy should have first be backtested. The results may then be plotted with the -s argument: -``` -python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// + +``` bash +python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// ``` ## Plot profit The profit plotter show a picture with three plots: + 1) Average closing price for all pairs 2) The summarized profit made by backtesting. Note that this is not the real-world profit, but @@ -76,13 +92,14 @@ that makes profit spikes. Usage for the profit plotter: -``` -script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] +``` bash +python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] ``` The `-p` pair argument, can be used to plot a single pair Example -``` + +``` bash python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC ``` From 8b028068bbe22b3aa0988c830aeb85316ee844e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 07:06:26 +0200 Subject: [PATCH 104/134] Fix typos, add section for custom indicators --- docs/plotting.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 20183ab9c..eb72b0502 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,6 +1,6 @@ # Plotting -This page explains how to plot prices, indicator, profits. +This page explains how to plot prices, indicators and profits. ## Installation @@ -10,8 +10,6 @@ Plotting scripts use Plotly library. Install/upgrade it with: pip install -U -r requirements-plot.txt ``` -At least version 2.3.0 is required. - ## Plot price and indicators Usage for the price plotter: @@ -26,8 +24,14 @@ Example python3 scripts/plot_dataframe.py -p BTC/ETH ``` -The `-p` pairs argument, can be used to specify -pairs you would like to plot. +The `-p` pairs argument can be used to specify pairs you would like to plot. + +Specify custom indicators. +Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd +``` ### Advanced use @@ -57,13 +61,13 @@ To plot trades stored in a database use `--db-url` argument: python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -To polt trades from a backtesting result, use `--export-filename ` +To plot trades from a backtesting result, use `--export-filename ` ``` bash python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH ``` -To plot a test strategy the strategy should have first be backtested. +To plot a custom strategy the strategy should have first be backtested. The results may then be plotted with the -s argument: ``` bash @@ -72,7 +76,7 @@ python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_dat ## Plot profit -The profit plotter show a picture with three plots: +The profit plotter shows a picture with three plots: 1) Average closing price for all pairs 2) The summarized profit made by backtesting. From 89f44c10a1d17e673a105b062d98857e0ee948ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 19:20:41 +0200 Subject: [PATCH 105/134] Fix grammar error --- docs/plotting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index eb72b0502..6dc3d13b1 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -87,7 +87,7 @@ The profit plotter shows a picture with three plots: The first graph is good to get a grip of how the overall market progresses. -The second graph will show how you algorithm works or doesnt. +The second graph will show how your algorithm works or doesn't. Perhaps you want an algorithm that steadily makes small profits, or one that acts less seldom, but makes big swings. From 55bdd2643954a38cfe6ebb144edfc04b84339894 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 May 2019 19:25:01 +0200 Subject: [PATCH 106/134] Edgecli -> Edge for Runmode and start_edge() --- freqtrade/arguments.py | 4 ++-- freqtrade/optimize/__init__.py | 4 ++-- freqtrade/state.py | 4 ++-- freqtrade/tests/optimize/test_edge_cli.py | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index d6f0063d0..ddc0dc489 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -340,7 +340,7 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import start_backtesting, start_hyperopt, start_edgecli + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge subparsers = self.parser.add_subparsers(dest='subparser') @@ -352,7 +352,7 @@ class Arguments(object): # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=start_edgecli) + edge_cmd.set_defaults(func=start_edge) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index cb01950b4..475aaa82f 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -99,7 +99,7 @@ def start_hyperopt(args: Namespace) -> None: # Same in Edge and Backtesting start() functions. -def start_edgecli(args: Namespace) -> None: +def start_edge(args: Namespace) -> None: """ Start Edge script :param args: Cli args from Arguments() @@ -107,7 +107,7 @@ def start_edgecli(args: Namespace) -> None: """ from freqtrade.optimize.edge_cli import EdgeCli # Initialize configuration - config = setup_configuration(args, RunMode.EDGECLI) + config = setup_configuration(args, RunMode.EDGE) logger.info('Starting freqtrade in Edge mode') # Initialize Edge object diff --git a/freqtrade/state.py b/freqtrade/state.py index b69c26cb5..ce2683a77 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -18,11 +18,11 @@ class State(Enum): class RunMode(Enum): """ Bot running mode (backtest, hyperopt, ...) - can be "live", "dry-run", "backtest", "edgecli", "hyperopt". + can be "live", "dry-run", "backtest", "edge", "hyperopt". """ LIVE = "live" DRY_RUN = "dry_run" BACKTEST = "backtest" - EDGECLI = "edgecli" + EDGE = "edge" HYPEROPT = "hyperopt" OTHER = "other" # Used for plotting scripts and test diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index dc40cc85c..5d16b0f2d 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize import start_edgecli, setup_configuration +from freqtrade.optimize import start_edge, setup_configuration from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -28,8 +28,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args), RunMode.EDGECLI) - assert config['runmode'] == RunMode.EDGECLI + config = setup_configuration(get_args(args), RunMode.EDGE) + assert config['runmode'] == RunMode.EDGE assert 'max_open_trades' in config assert 'stake_currency' in config @@ -68,14 +68,14 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args), RunMode.EDGECLI) + config = setup_configuration(get_args(args), RunMode.EDGE) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert config['runmode'] == RunMode.EDGECLI + assert config['runmode'] == RunMode.EDGE assert log_has( 'Using data folder: {} ...'.format(config['datadir']), caplog.record_tuples @@ -107,7 +107,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: 'edge' ] args = get_args(args) - start_edgecli(args) + start_edge(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples From 536c8fa4549715ab6bd63d35ef7b0b4a1223f385 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:04:39 +0300 Subject: [PATCH 107/134] move python version check to the top --- freqtrade/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 809ab3c7a..35fbccfa3 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,8 +3,14 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ -import logging + import sys +# check min. python version +if sys.version_info < (3, 6): + sys.exit("Freqtrade requires Python version >= 3.6") + +# flake8: noqa E402 +import logging from argparse import Namespace from typing import List @@ -26,10 +32,6 @@ def main(sysargv: List[str]) -> None: worker = None return_code = 1 - # check min. python version - if sys.version_info < (3, 6): - raise SystemError("Freqtrade requires Python version >= 3.6") - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' From 58477dcd8205784ba58c69233229f285fb272c20 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:25:19 +0300 Subject: [PATCH 108/134] cleanup: return after cmd removed in main() --- freqtrade/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 35fbccfa3..d8c447800 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -44,11 +44,10 @@ def main(sysargv: List[str]) -> None: args.func(args) # TODO: fetch return_code as returned by the command function here return_code = 0 - return - - # Load and run worker - worker = Worker(args) - worker.run() + else: + # Load and run worker + worker = Worker(args) + worker.run() except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') From db2e6f2d1c9a25c077f660c1738bc65b64f26a7e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 28 May 2019 23:25:53 +0300 Subject: [PATCH 109/134] tests adjusted --- freqtrade/tests/test_main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e4ffc5fae..9e7cc4a66 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -20,7 +20,9 @@ def test_parse_args_backtesting(mocker) -> None: further argument parsing is done in test_arguments.py """ backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) - main(['backtesting']) + # it's sys.exit(0) at the end of backtesting + with pytest.raises(SystemExit): + main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] @@ -33,7 +35,9 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) - main(['hyperopt']) + # it's sys.exit(0) at the end of hyperopt + with pytest.raises(SystemExit): + main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == ['config.json'] From ea83b2b1d0542adc299513c2876963a433cea987 Mon Sep 17 00:00:00 2001 From: Misagh Date: Wed, 29 May 2019 14:17:09 +0200 Subject: [PATCH 110/134] legacy code removed. --- user_data/strategies/test_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 3cb78842f..66a5f8c09 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -51,7 +51,7 @@ class TestStrategy(IStrategy): ticker_interval = '5m' # run "populate_indicators" only for new candle - ta_on_candle = False + process_only_new_candles = False # Experimental settings (configuration will overide these if set) use_sell_signal = False From 7406edfd8fcad1b5104e470f43c862cf70ed4cc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 14:51:50 +0200 Subject: [PATCH 111/134] Move set_loggers to main() --- freqtrade/__main__.py | 1 - freqtrade/main.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 7d271dfd1..628fc930f 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -11,5 +11,4 @@ import sys from freqtrade import main if __name__ == '__main__': - main.set_loggers() main.main(sys.argv[1:]) diff --git a/freqtrade/main.py b/freqtrade/main.py index d8c447800..d0e783808 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -28,7 +28,24 @@ def main(sysargv: List[str]) -> None: This function will initiate the bot and start the trading loop. :return: None """ + set_loggers() + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() + + # A subcommand has been issued. + # Means if Backtesting or Hyperopt have been called we exit the bot + if hasattr(args, 'func'): + args.func(args) + return + + worker = None + return_code = 1 try: + set_loggers() + worker = None return_code = 1 @@ -64,5 +81,4 @@ def main(sysargv: List[str]) -> None: if __name__ == '__main__': - set_loggers() main(sys.argv[1:]) From 17d614c66a4ab39484b13582061e846d637d7b4f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:07:52 +0200 Subject: [PATCH 112/134] Remove binary script - allow None arguemnts --- bin/freqtrade | 7 ------- freqtrade/__main__.py | 4 +--- freqtrade/main.py | 4 ++-- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100755 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade deleted file mode 100755 index e7ae7a4ca..000000000 --- a/bin/freqtrade +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from freqtrade.main import main, set_loggers -set_loggers() -main(sys.argv[1:]) diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 628fc930f..97ed9ae67 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -6,9 +6,7 @@ To launch Freqtrade as a module > python -m freqtrade (with Python >= 3.6) """ -import sys - from freqtrade import main if __name__ == '__main__': - main.main(sys.argv[1:]) + main.main() diff --git a/freqtrade/main.py b/freqtrade/main.py index d0e783808..9fc8c9d0c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -23,7 +23,7 @@ from freqtrade.worker import Worker logger = logging.getLogger('freqtrade') -def main(sysargv: List[str]) -> None: +def main(sysargv: List[str] = None) -> None: """ This function will initiate the bot and start the trading loop. :return: None @@ -81,4 +81,4 @@ def main(sysargv: List[str]) -> None: if __name__ == '__main__': - main(sys.argv[1:]) + main() From c5ef700eb700dd30d58d6b4d0236e1ec352e361d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:08:35 +0200 Subject: [PATCH 113/134] Use autogenerated entrypoint --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35fdb2938..ca2f81d1f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ setup(name='freqtrade', author_email='michael.egger@tsn.at', license='GPLv3', packages=['freqtrade'], - scripts=['bin/freqtrade'], setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ @@ -43,6 +42,11 @@ setup(name='freqtrade', ], include_package_data=True, zip_safe=False, + entry_points={ + 'console_scripts': [ + 'freqtrade = freqtrade.main:main', + ], + }, classifiers=[ 'Programming Language :: Python :: 3.6', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', From 22144d89fc3cf4c69a7bef6db3b9a33b0dd9e6ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 May 2019 15:31:30 +0200 Subject: [PATCH 114/134] Fix mypy error --- freqtrade/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index ddc0dc489..89b587c6f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -27,7 +27,7 @@ class Arguments(object): Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: List[str], description: str) -> None: + def __init__(self, args: Optional[List[str]], description: str) -> None: self.args = args self.parsed_arg: Optional[argparse.Namespace] = None self.parser = argparse.ArgumentParser(description=description) From 9e4dd6f37f7f698c4ae92a91b2293bd9fa2a46ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 May 2019 19:27:24 +0200 Subject: [PATCH 115/134] Read bin/freqtrade with deprecation warning --- bin/freqtrade | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade new file mode 100755 index 000000000..b9e3a7008 --- /dev/null +++ b/bin/freqtrade @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import sys +import warnings + +from freqtrade.main import main, set_loggers + +set_loggers() + +warnings.warn( + "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", + DeprecationWarning) +main(sys.argv[1:]) From 7b367818fc79366c883ee06a6de6b8b57b75e95e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 19:46:46 +0200 Subject: [PATCH 116/134] Remove duplicate code --- freqtrade/main.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 9fc8c9d0c..4b1decdc5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -28,21 +28,7 @@ def main(sysargv: List[str] = None) -> None: This function will initiate the bot and start the trading loop. :return: None """ - set_loggers() - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args: Namespace = arguments.get_parsed_arg() - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot - if hasattr(args, 'func'): - args.func(args) - return - - worker = None - return_code = 1 try: set_loggers() From d7bebc4385cf833aa69732343ec7d62fd36f6762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 19:54:47 +0200 Subject: [PATCH 117/134] persistence.init does not need the config dict --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence.py | 5 ++--- freqtrade/tests/test_persistence.py | 24 ++++++++++++------------ scripts/plot_dataframe.py | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8b29d6d40..1a5187d8c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,7 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config) + persistence.init(self.config.get('db_url', None), self.config.get('dry_run', False)) # Set initial bot state from config initial_state = self.config.get('initial_state') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e64e0b89c..5218b793a 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,7 +25,7 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(config: Dict) -> None: +def init(db_url: str, dry_run: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers @@ -33,7 +33,6 @@ def init(config: Dict) -> None: :param config: config to use :return: None """ - db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -57,7 +56,7 @@ def init(config: Dict) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if config.get('dry_run', False) and db_url != 'sqlite://': + if dry_run and db_url != 'sqlite://': clean_dry_run_db() diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 8c15fa8e8..7e47abf39 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -13,12 +13,12 @@ from freqtrade.tests.conftest import log_has @pytest.fixture(scope='function') def init_persistence(default_conf): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_create_session(default_conf): # Check if init create a session - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'Session' in type(Trade.session).__name__ @@ -28,7 +28,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -37,7 +37,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -46,7 +46,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -57,7 +57,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' @@ -336,8 +336,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit_percent(fee=0.003) == 0.06147824 +@pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -424,7 +424,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(create_table_old) engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -497,7 +497,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -566,7 +566,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -668,8 +668,8 @@ def test_adjust_min_max_rates(fee): assert trade.min_rate == 0.96 +@pytest.mark.usefixtures("init_persistence") def test_get_open(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -713,8 +713,8 @@ def test_get_open(default_conf, fee): assert len(Trade.get_open_trades()) == 2 +@pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ba549deb5..49ae857b6 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,7 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(_CONF) + persistence.init(args.db_url, True) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From c2f6897d8b722a1c92cd845f63adfe1270c7bff7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 20:10:48 +0200 Subject: [PATCH 118/134] Move download of live data to load_data Avoids code duplication in backtesting and plot_dataframe --- freqtrade/data/history.py | 26 +++++++++++++++++--------- freqtrade/optimize/backtesting.py | 29 +++++++++++------------------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index e0f9f67db..29a7f3478 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -122,21 +122,29 @@ def load_data(datadir: Optional[Path], refresh_pairs: bool = False, exchange: Optional[Exchange] = None, timerange: TimeRange = TimeRange(None, None, 0, 0), - fill_up_missing: bool = True) -> Dict[str, DataFrame]: + fill_up_missing: bool = True, + live: bool = False + ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs the given parameters :return: dict(:) """ result = {} + if live: + logger.info('Live: Downloading data for all defined pairs ...') + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) + result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + else: + logger.info('Using local backtesting data ...') - for pair in pairs: - hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, - datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, - fill_up_missing=fill_up_missing) - if hist is not None: - result[pair] = hist + for pair in pairs: + hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, + datadir=datadir, timerange=timerange, + refresh_pairs=refresh_pairs, + exchange=exchange, + fill_up_missing=fill_up_missing) + if hist is not None: + result[pair] = hist return result diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bdd42943b..76c6556fa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -401,24 +401,17 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - if self.config.get('live'): - logger.info('Downloading data for all pairs in whitelist ...') - self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs]) - data = {key[0]: value for key, value in self.exchange._klines.items()} - - else: - logger.info('Using local backtesting data (using whitelist in given config) ...') - - timerange = Arguments.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - data = history.load_data( - datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, - pairs=pairs, - ticker_interval=self.ticker_interval, - refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, - timerange=timerange - ) + timerange = Arguments.parse_timerange(None if self.config.get( + 'timerange') is None else str(self.config.get('timerange'))) + data = history.load_data( + datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, + pairs=pairs, + ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, + timerange=timerange, + live=self.config.get('live', False) + ) if not data: logger.critical("No data found. Terminating.") From 15984b5c432b0f19fc1a8ace8ab6f36427b16e02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 May 2019 20:25:07 +0200 Subject: [PATCH 119/134] Adjust some tests - implement new "live" method to plot_script --- freqtrade/data/history.py | 13 ++++++--- freqtrade/tests/data/test_history.py | 29 +++++++++++++++++++- freqtrade/tests/optimize/test_backtesting.py | 7 ++--- scripts/plot_dataframe.py | 24 ++++++---------- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 29a7f3478..2dacce8c6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -129,11 +129,16 @@ def load_data(datadir: Optional[Path], Loads ticker history data for a list of pairs the given parameters :return: dict(:) """ - result = {} + result: Dict[str, DataFrame] = {} if live: - logger.info('Live: Downloading data for all defined pairs ...') - exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) - result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + if exchange: + logger.info('Live: Downloading data for all defined pairs ...') + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) + result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + else: + raise OperationalException( + "Exchange needs to be initialized when using live data." + ) else: logger.info('Using local backtesting data ...') diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 4d70d4cdd..a13bc34af 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -5,6 +5,7 @@ import os import uuid from pathlib import Path from shutil import copyfile +from unittest.mock import MagicMock import arrow import pytest @@ -20,7 +21,8 @@ from freqtrade.data.history import (download_pair_history, from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.strategy.default_strategy import DefaultStrategy -from freqtrade.tests.conftest import get_patched_exchange, log_has, patch_exchange +from freqtrade.tests.conftest import (get_patched_exchange, log_has, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -136,6 +138,31 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau _clean_test_file(file) +def test_load_data_live(default_conf, mocker, caplog) -> None: + refresh_mock = MagicMock() + mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) + exchange = get_patched_exchange(mocker, default_conf) + + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + live=True, + exchange=exchange) + assert refresh_mock.call_count == 1 + assert len(refresh_mock.call_args_list[0][0][0]) == 2 + assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples) + + +def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None: + + with pytest.raises(OperationalException, + match=r'Exchange needs to be initialized when using live data.'): + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + exchange=None, + live=True, + ) + + def test_testdata_path() -> None: assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5b42cae34..3f88a8d6c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -105,7 +105,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None): + timerange=None, exchange=None, live=False): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)} return pairdata @@ -492,7 +492,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result exists = [ - 'Using local backtesting data (using whitelist in given config) ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' @@ -857,7 +856,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' @@ -916,7 +915,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ba549deb5..27932b559 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -139,21 +139,15 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): ticker_interval = strategy.ticker_interval timerange = Arguments.parse_timerange(args.timerange) - tickers = {} - if args.live: - logger.info('Downloading pairs.') - exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) - for pair in pairs: - tickers[pair] = exchange.klines((pair, ticker_interval)) - else: - tickers = history.load_data( - datadir=Path(str(_CONF.get("datadir"))), - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), - timerange=timerange, - exchange=Exchange(_CONF) - ) + tickers = history.load_data( + datadir=Path(str(_CONF.get("datadir"))), + pairs=pairs, + ticker_interval=ticker_interval, + refresh_pairs=_CONF.get('refresh_pairs', False), + timerange=timerange, + exchange=Exchange(_CONF), + live=args.live, + ) # No ticker found, impossible to download, len mismatch for pair, data in tickers.copy().items(): From d6cf3144813a6f1d14a50c195f109a1ff027c9bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 06:30:06 +0200 Subject: [PATCH 120/134] Don't default to false for init() --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1a5187d8c..dfffc21b3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,7 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config.get('db_url', None), self.config.get('dry_run', False)) + persistence.init(self.config.get('db_url', None), self.config.get('dry_run')) # Set initial bot state from config initial_state = self.config.get('initial_state') From b6e8fecbf50b3f22c7efdbabe7e3f75230d8c166 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 May 2019 06:31:34 +0200 Subject: [PATCH 121/134] Change persistence.init parameter It should describe what it does --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence.py | 8 +++++--- scripts/plot_dataframe.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dfffc21b3..3f8c1e106 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,8 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config.get('db_url', None), self.config.get('dry_run')) + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) # Set initial bot state from config initial_state = self.config.get('initial_state') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 5218b793a..3d86d4f4d 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,12 +25,14 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(db_url: str, dry_run: bool = False) -> None: +def init(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates - :param config: config to use + :param db_url: Database to use + :param clean_open_orders: Remove open orders from the database. + Useful for dry-run or if all orders have been reset on the exchange. :return: None """ kwargs = {} @@ -56,7 +58,7 @@ def init(db_url: str, dry_run: bool = False) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if dry_run and db_url != 'sqlite://': + if clean_open_orders and db_url != 'sqlite://': clean_dry_run_db() diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 49ae857b6..74e8573e5 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,7 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(args.db_url, True) + persistence.init(args.db_url, False) columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From 6b144150c757e81cda5db8e66f681859fce2f02c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 20:38:04 +0300 Subject: [PATCH 122/134] fix handling of SystemExit --- freqtrade/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 4b1decdc5..f693bba5c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -52,13 +52,15 @@ def main(sysargv: List[str] = None) -> None: worker = Worker(args) worker.run() + except SystemExit as e: + return_code = e except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: + except Exception: logger.exception('Fatal exception!') finally: if worker: From e4e22167bb575e608775caac2b93281d199891c8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 30 May 2019 21:00:16 +0300 Subject: [PATCH 123/134] make mypy happy --- freqtrade/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index f693bba5c..6f073f5d4 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -12,7 +12,7 @@ if sys.version_info < (3, 6): # flake8: noqa E402 import logging from argparse import Namespace -from typing import List +from typing import Any, List from freqtrade import OperationalException from freqtrade.arguments import Arguments @@ -29,12 +29,11 @@ def main(sysargv: List[str] = None) -> None: :return: None """ + return_code: Any = 1 + worker = None try: set_loggers() - worker = None - return_code = 1 - arguments = Arguments( sysargv, 'Free, open source crypto trading bot' From 338f2a2322b87affc4c34f63517f31773e4ef131 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Jun 2019 06:26:03 +0200 Subject: [PATCH 124/134] Use kwarg to call persistence.init() --- scripts/plot_dataframe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 74e8573e5..9316c953b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,8 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(args.db_url, False) + persistence.init(args.db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] From 107c3beb200fefe805f97b02223b3e0159935af2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Jun 2019 15:28:29 +0200 Subject: [PATCH 125/134] Fix test-failure introduced in #1891 --- freqtrade/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 59989d604..a907b33ed 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -96,7 +96,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - persistence.init(config) + persistence.init(config['db_url']) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) From bd8edd61fd29f304bdfdb6c03fcc81b6a45cfaf4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:12 +0200 Subject: [PATCH 126/134] Update numpy from 1.16.3 to 1.16.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da87f56d9..52442fb19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Load common requirements -r requirements-common.txt -numpy==1.16.3 +numpy==1.16.4 pandas==0.24.2 scipy==1.3.0 From c04a8a102469a099230e09c85b8fb4e48798c5d0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:13 +0200 Subject: [PATCH 127/134] Update ccxt from 1.18.578 to 1.18.615 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 434944aad..7620ddada 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.578 +ccxt==1.18.615 SQLAlchemy==1.3.3 python-telegram-bot==11.1.0 arrow==0.13.2 From 51113dae0e2d21fee9095ddaa5b03ddf931ff86d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:16 +0200 Subject: [PATCH 128/134] Update sqlalchemy from 1.3.3 to 1.3.4 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 7620ddada..ee8b08b75 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.18.615 -SQLAlchemy==1.3.3 +SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 arrow==0.13.2 cachetools==3.1.1 From 4ef8a74977acf7fb55a8350269f2be00e7a616fd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:19 +0200 Subject: [PATCH 129/134] Update arrow from 0.13.2 to 0.14.1 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index ee8b08b75..bf21c6e59 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.18.615 SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 -arrow==0.13.2 +arrow==0.14.1 cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore From 3c1ae07f92d1399cbc336f37d32db1afb9801050 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:20 +0200 Subject: [PATCH 130/134] Update flask from 1.0.2 to 1.0.3 --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index bf21c6e59..9e854e4af 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -29,4 +29,4 @@ python-rapidjson==0.7.1 sdnotify==0.3.2 # Api server -flask==1.0.2 +flask==1.0.3 From a132517f0abeb60071608ba39b405cdaa7838f60 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:24 +0200 Subject: [PATCH 131/134] Update pytest from 4.5.0 to 4.6.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fa52a4869..531d99940 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.5.0 +pytest==4.6.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 From f75e97e9b0e15cc114101051ab4d57a038b7ddc3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:25 +0200 Subject: [PATCH 132/134] Update coveralls from 1.7.0 to 1.8.0 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 531d99940..effa714e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,5 @@ pytest==4.6.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 -coveralls==1.7.0 +coveralls==1.8.0 mypy==0.701 From 7134273918e927d010283a4626380c7cdcf532c5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Jun 2019 17:19:26 +0200 Subject: [PATCH 133/134] Update plotly from 3.9.0 to 3.10.0 --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 23daee258..d4e4fc165 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.9.0 +plotly==3.10.0 From 5273540a93fd8ce33b998a2674024a0a1d133702 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Jun 2019 19:32:31 +0200 Subject: [PATCH 134/134] Fix test failure (double-trailing newlines are removed now) --- freqtrade/tests/optimize/test_hyperopt.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index b41f8ac36..a51d74dbb 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -372,20 +372,21 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: ) patch_exchange(mocker) - default_conf.update({'config': 'config.json.example'}) - default_conf.update({'epochs': 1}) - default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) - default_conf.update({'hyperopt_jobs': 1}) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() parallel.assert_called_once() - - assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text + assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples) assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 def test_format_results(hyperopt):