From efbeabf14101d205b9035000c02197914f909b2b Mon Sep 17 00:00:00 2001 From: creslinux Date: Fri, 8 Jun 2018 18:35:01 +0000 Subject: [PATCH 01/31] Added Local RPC client - added only "Daily" call so far, submitting for early review/feedback This depends on zerorpc as a requirement. simple examples here: http://www.zerorpc.io/ Installed with pip3 install zerorpc localRCP is enabled/disabled from within config.json e.g "localrpc": { "enabled": true }, The server is enabled from within existing rpc manager and makes use of the existing superclass (RPC) Though making use of the existing hardwork done in rpc.py It *should be easy to add the other Telegram calls into local_rpy_server.py The server is wrapped in a thread to be non-blocking The server and client accept serialised calls or not, used in daily to return json The client can be used from command line or in a python script As example, from cmdline for last 3 days Daily /Users/creslin/PycharmProjects/freqtrade_new/.env/bin/zerorpc tcp://127.0.0.1:4242 daily 3 connecting to "tcp://127.0.0.1:4242" False ('[\n' ' [\n' ' "2018-06-08",\n' ' "0.00000000 BTC",\n' ' "0.000 USDT",\n' ' "0 trade"\n' ' ],\n' ' [\n' ' "2018-06-07",\n' ' "0.00000000 BTC",\n' ' "0.000 USDT",\n' ' "0 trade"\n' ' ],\n' ' [\n' ' "2018-06-06",\n' ' "0.00000000 BTC",\n' ' "0.000 USDT",\n' ' "0 trade"\n' ' ]\n' ']') Programitcally this would be: import zerorpc c = zerorpc.Client() c.connect("tcp://127.0.0.1:4242") for item in c.daily(3): print item --- freqtrade/rpc/local_rpc_server.py | 83 +++++++++++++++++++++++++++++++ freqtrade/rpc/rpc_manager.py | 8 +++ requirements.txt | 3 ++ 3 files changed, 94 insertions(+) create mode 100644 freqtrade/rpc/local_rpc_server.py diff --git a/freqtrade/rpc/local_rpc_server.py b/freqtrade/rpc/local_rpc_server.py new file mode 100644 index 000000000..50b6b3a6d --- /dev/null +++ b/freqtrade/rpc/local_rpc_server.py @@ -0,0 +1,83 @@ +import threading +import time +import zerorpc +import logging +import json + +from freqtrade.rpc.rpc import RPC + + +logger = logging.getLogger(__name__) + +class LocalRPCControls(object): + """ + zeroRPC - allows local cmdline calls to super class in rpc.py + as used by Telegram.py + """ + + def __init__(self, freqtrade) -> None: + """ + Initializes all enabled rpc modules + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + self.freqtrade = freqtrade + self._config = freqtrade.config + + # # Example of calling none serialed call + # # without decorator - left if as template while in dev for me + # def add_42(self, n): + # """ Add 42 to an integer argument to make it cooler, and return the + # result. """ + # n = int(n) + # r = n + 42 + # s = str(r) + # return s + + @zerorpc.stream + def daily(self, timescale): + logger.info("LocalRPC - Daily Command Called") + timescale = int(timescale) + + (error, stats) = RPC.rpc_daily_profit(self, timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + #Everything in stats to a string, serialised, then back to client. + stats = json.dumps(stats, indent=4, sort_keys=True, default=str) + return(error, stats) + +class LocalRPCSuperWrap(RPC): + """ + Telegram, this class send messages to Telegram + """ + def __init__(self, freqtrade) -> None: + """ + Init the LocalRPCServer call, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + """ Constructor + :type interval: int + :param interval: Check interval, in seconds + """ + self.interval = int(1) + + thread = threading.Thread(target=self.run, args=(freqtrade,)) # extra comma as ref ! Tuple + thread.daemon = True # Daemonize thread + thread.start() # Start the execution + + def run(self, freqtrade): + """ Method that runs forever """ + self._config = freqtrade.config + + # TODO add IP address / port to bind to in config.json and use in below. + while True: + # Do something + logger.info('Starting Local RPC Listener') + s = zerorpc.Server(LocalRPCControls(freqtrade)) + s.bind("tcp://0.0.0.0:4242") + s.run() + time.sleep(self.interval) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 58e9bf2b9..b89e8a0d5 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -3,8 +3,10 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) """ from typing import Any, List import logging +import time from freqtrade.rpc.telegram import Telegram +from freqtrade.rpc.local_rpc_server import LocalRPCSuperWrap logger = logging.getLogger(__name__) @@ -36,6 +38,12 @@ class RPCManager(object): self.registered_modules.append('telegram') self.telegram = Telegram(self.freqtrade) + # Added another RPC client - for cmdline local client. + # Uses existing superclass RPC build for Telegram + if self.freqtrade.config['localrpc'].get('enabled', False): + self.localRPC = LocalRPCSuperWrap(self.freqtrade) + time.sleep(1) + def cleanup(self) -> None: """ Stops all enabled rpc modules diff --git a/requirements.txt b/requirements.txt index 5f5183321..4f4924225 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,6 @@ coinmarketcap==5.0.3 # Required for plotting data #plotly==2.3.0 + +#Added for localRCP +zerorpc== \ No newline at end of file From e5e64a7035172cce6e09d4eaf8bbc014bf0a5557 Mon Sep 17 00:00:00 2001 From: creslinux Date: Wed, 13 Jun 2018 22:18:49 +0000 Subject: [PATCH 02/31] Implemented local restful flask service and provided cmdline client Added only the "Daily" call so far, submitting for early review/feedback Called as example "./rest_client.py daily 3" This depends on listed as requirements. Flask==1.0.2 flask-jsonpify==1.5.0 (will do later) flask-restful==0.3.6 TODO: make loading optional, cleanly unload on close unit tests, take feedback, tidy output, add other Telegram functions, onwards local rest server is enabled/disabled from within config.json. E.g "localrest": { "enabled": true }, The server is enabled from within existing rpc manager and makes use of the existing superclass (RPC) Through making use of the existing hard work done in rpc.py It *should be easy to add the other Telegram calls into local_rpc_server.py The server is wrapped in a thread to be non-blocking The server and client accept serialised calls or not, used in daily to return json The client can be used from command line or in a python client script As example, from cmdline for last 3 days Daily DannyMBP:rpc creslin$ ./rest_client.py daily 3 [ [ "2018-06-13", "0.00000000 USDT", "0.000 USD", "0 trade" ], [ "2018-06-12", "0.00000000 USDT", "0.000 USD", "0 trade" ], [ "2018-06-11", "0.00000000 USDT", "0.000 USD", "0 trade" ] ] --- ...cal_rpc_server.py => local_rest_server.py} | 62 +++++++++---------- freqtrade/rpc/rest_client.py | 23 +++++++ freqtrade/rpc/rpc_manager.py | 6 +- requirements.txt | 6 +- 4 files changed, 61 insertions(+), 36 deletions(-) rename freqtrade/rpc/{local_rpc_server.py => local_rest_server.py} (57%) create mode 100755 freqtrade/rpc/rest_client.py diff --git a/freqtrade/rpc/local_rpc_server.py b/freqtrade/rpc/local_rest_server.py similarity index 57% rename from freqtrade/rpc/local_rpc_server.py rename to freqtrade/rpc/local_rest_server.py index 50b6b3a6d..940cc5942 100644 --- a/freqtrade/rpc/local_rpc_server.py +++ b/freqtrade/rpc/local_rest_server.py @@ -1,19 +1,19 @@ import threading -import time -import zerorpc import logging import json +from flask import Flask, request +from flask_restful import Resource, Api +from json import dumps from freqtrade.rpc.rpc import RPC logger = logging.getLogger(__name__) -class LocalRPCControls(object): - """ - zeroRPC - allows local cmdline calls to super class in rpc.py - as used by Telegram.py - """ + +class Daily(Resource): + # called by http://127.0.0.1:/daily?timescale=7 + # where 7 is the number of days to report back with. def __init__(self, freqtrade) -> None: """ @@ -24,18 +24,9 @@ class LocalRPCControls(object): self.freqtrade = freqtrade self._config = freqtrade.config - # # Example of calling none serialed call - # # without decorator - left if as template while in dev for me - # def add_42(self, n): - # """ Add 42 to an integer argument to make it cooler, and return the - # result. """ - # n = int(n) - # r = n + 42 - # s = str(r) - # return s - @zerorpc.stream - def daily(self, timescale): + def get(self): + timescale = request.args.get('timescale') logger.info("LocalRPC - Daily Command Called") timescale = int(timescale) @@ -43,18 +34,21 @@ class LocalRPCControls(object): self._config['stake_currency'], self._config['fiat_display_currency'] ) + if error == False: + stats = dumps(stats, indent=4, sort_keys=True, default=str) + return stats + else: + json.dumps(error) + return error - #Everything in stats to a string, serialised, then back to client. - stats = json.dumps(stats, indent=4, sort_keys=True, default=str) - return(error, stats) -class LocalRPCSuperWrap(RPC): +class LocalRestSuperWrap(RPC): """ - Telegram, this class send messages to Telegram + This class is for REST cmd line client """ def __init__(self, freqtrade) -> None: """ - Init the LocalRPCServer call, and init the super class RPC + Init the LocalRestServer call, and init the super class RPC :param freqtrade: Instance of a freqtrade bot :return: None """ @@ -73,11 +67,17 @@ class LocalRPCSuperWrap(RPC): """ Method that runs forever """ self._config = freqtrade.config + # TODO add IP address / port to bind to in config.json and use in below. - while True: - # Do something - logger.info('Starting Local RPC Listener') - s = zerorpc.Server(LocalRPCControls(freqtrade)) - s.bind("tcp://0.0.0.0:4242") - s.run() - time.sleep(self.interval) + logger.info('Starting Local Rest Server') + + my_freqtrade = freqtrade + app = Flask(__name__) + api = Api(app) + + # Our resources for restful apps go here, pass freqtrade object across + api.add_resource(Daily, '/daily', methods=['GET'], + resource_class_kwargs={'freqtrade': my_freqtrade}) # Route for returning daily + + #run the server + app.run(port='5002') diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py new file mode 100755 index 000000000..5b39b7a0b --- /dev/null +++ b/freqtrade/rpc/rest_client.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +""" +Simple command line client into RPC commands +Can be used as an alternate to Telegram +""" + +from requests import get +from sys import argv + +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('- 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") + + diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index b89e8a0d5..2dfb119fe 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -6,7 +6,7 @@ import logging import time from freqtrade.rpc.telegram import Telegram -from freqtrade.rpc.local_rpc_server import LocalRPCSuperWrap +from freqtrade.rpc.local_rest_server import LocalRestSuperWrap logger = logging.getLogger(__name__) @@ -40,8 +40,8 @@ class RPCManager(object): # Added another RPC client - for cmdline local client. # Uses existing superclass RPC build for Telegram - if self.freqtrade.config['localrpc'].get('enabled', False): - self.localRPC = LocalRPCSuperWrap(self.freqtrade) + if self.freqtrade.config['rest_cmd'].get('enabled', False): + self.localRPC = LocalRestSuperWrap(self.freqtrade) time.sleep(1) def cleanup(self) -> None: diff --git a/requirements.txt b/requirements.txt index 4f4924225..34373b22f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,5 +24,7 @@ coinmarketcap==5.0.3 # Required for plotting data #plotly==2.3.0 -#Added for localRCP -zerorpc== \ No newline at end of file +#Added for local rest client +Flask==1.0.2 +flask-jsonpify==1.5.0 +flask-restful==0.3.6 From 6bb1ad288e6d74b749e11ec7d8490ebe1ab1b466 Mon Sep 17 00:00:00 2001 From: creslinux Date: Wed, 13 Jun 2018 22:26:21 +0000 Subject: [PATCH 03/31] Implemented local restful flask service and provided cmdline client Added only the "Daily" call so far, submitting for early review/feedback Called as example "./rest_client.py daily 3" This depends on listed as requirements. Flask==1.0.2 flask-jsonpify==1.5.0 (will do later) flask-restful==0.3.6 TODO: make loading optional, cleanly unload on close unit tests, take feedback, tidy output, add other Telegram functions, onwards local rest server is enabled/disabled from within config.json. E.g "localrest": { "enabled": true }, The server is enabled from within existing rpc manager and makes use of the existing superclass (RPC) Through making use of the existing hard work done in rpc.py It *should be easy to add the other Telegram calls into local_rpc_server.py The server is wrapped in a thread to be non-blocking The server and client accept serialised calls or not, used in daily to return json The client can be used from command line or in a python client script As example, from cmdline for last 3 days Daily DannyMBP:rpc creslin$ ./rest_client.py daily 3 [ [ "2018-06-13", "0.00000000 USDT", "0.000 USD", "0 trade" ], [ "2018-06-12", "0.00000000 USDT", "0.000 USD", "0 trade" ], [ "2018-06-11", "0.00000000 USDT", "0.000 USD", "0 trade" ] ] --- freqtrade/rpc/local_rest_server.py | 2 +- freqtrade/rpc/rest_client.py | 2 +- freqtrade/rpc/rpc_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/local_rest_server.py b/freqtrade/rpc/local_rest_server.py index 940cc5942..0c9fad16b 100644 --- a/freqtrade/rpc/local_rest_server.py +++ b/freqtrade/rpc/local_rest_server.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) class Daily(Resource): # called by http://127.0.0.1:/daily?timescale=7 - # where 7 is the number of days to report back with. + # where 7 is the number of days to report back with def __init__(self, freqtrade) -> None: """ diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 5b39b7a0b..928e1798c 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3. """ Simple command line client into RPC commands Can be used as an alternate to Telegram diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 2dfb119fe..ef71ff3a9 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -1,5 +1,5 @@ """ -This module contains class to manage RPC communications (Telegram, Slack, ...) +This module contains class to manage RPC communications (Telegram, Slack, ....) """ from typing import Any, List import logging From faf0a4973d4cdf9921874dad7e61044336973ea0 Mon Sep 17 00:00:00 2001 From: creslin <34645187+creslinux@users.noreply.github.com> Date: Wed, 13 Jun 2018 22:41:43 +0000 Subject: [PATCH 04/31] Update rest_client.py --- freqtrade/rpc/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 928e1798c..5b39b7a0b 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3. +#!/usr/bin/env python3 """ Simple command line client into RPC commands Can be used as an alternate to Telegram From fb60f684f7f3385bd2dedf8807e9f2ffae5b730d Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 14 Jun 2018 15:38:26 +0000 Subject: [PATCH 05/31] Updated Stop and start calls added Along with refactoring to base line and use decorators. Also modules loaded optionally if enabled in config or not binds to ip / port set from config.json with warning if not localhost TODO: - use argparse in client, and generally clean client up - create unit test - documentation - extend to other RCP commands, after feedback --- freqtrade/rpc/local_rest_server.py | 122 ++++++++++++++++++----------- freqtrade/rpc/rest_client.py | 30 +++++++ freqtrade/rpc/rpc_manager.py | 65 ++++++--------- 3 files changed, 131 insertions(+), 86 deletions(-) diff --git a/freqtrade/rpc/local_rest_server.py b/freqtrade/rpc/local_rest_server.py index 0c9fad16b..24d9a6594 100644 --- a/freqtrade/rpc/local_rest_server.py +++ b/freqtrade/rpc/local_rest_server.py @@ -5,43 +5,11 @@ import json from flask import Flask, request from flask_restful import Resource, Api from json import dumps -from freqtrade.rpc.rpc import RPC +from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) - -class Daily(Resource): - # called by http://127.0.0.1:/daily?timescale=7 - # where 7 is the number of days to report back with - - def __init__(self, freqtrade) -> None: - """ - Initializes all enabled rpc modules - :param freqtrade: Instance of a freqtrade bot - :return: None - """ - self.freqtrade = freqtrade - self._config = freqtrade.config - - - def get(self): - timescale = request.args.get('timescale') - logger.info("LocalRPC - Daily Command Called") - timescale = int(timescale) - - (error, stats) = RPC.rpc_daily_profit(self, timescale, - self._config['stake_currency'], - self._config['fiat_display_currency'] - ) - if error == False: - stats = dumps(stats, indent=4, sort_keys=True, default=str) - return stats - else: - json.dumps(error) - return error - - class LocalRestSuperWrap(RPC): """ This class is for REST cmd line client @@ -61,23 +29,85 @@ class LocalRestSuperWrap(RPC): thread = threading.Thread(target=self.run, args=(freqtrade,)) # extra comma as ref ! Tuple thread.daemon = True # Daemonize thread - thread.start() # Start the execution + thread.start() # Start the execution + def run(self, freqtrade): """ Method that runs forever """ self._config = freqtrade.config - - - # TODO add IP address / port to bind to in config.json and use in below. - logger.info('Starting Local Rest Server') - - my_freqtrade = freqtrade app = Flask(__name__) - api = Api(app) - # Our resources for restful apps go here, pass freqtrade object across - api.add_resource(Daily, '/daily', methods=['GET'], - resource_class_kwargs={'freqtrade': my_freqtrade}) # Route for returning daily + """ + Define the application routes here + each Telegram command should have a like local substitute + """ + @app.route("/") + def hello(): + # For simple rest server testing via browser + # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status + # /status /table /performance /count, + # /start /stop /help' - #run the server - app.run(port='5002') + rest_cmds ='Commands implemented:
' \ + '/daily?timescale=7' \ + '
' \ + '/stop' \ + '
' \ + '/start' + return rest_cmds + + @app.route('/daily', methods=['GET']) + def daily(): + 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'] + ) + + stats = dumps(stats, indent=4, sort_keys=True, default=str) + return stats + except RPCException as e: + return e + + @app.route('/start', methods=['GET']) + def start(): + """ + Handler for /start. + Starts TradeThread + """ + msg = self._rpc_start() + print("msg is", msg) + return msg + + @app.route('/stop', methods=['GET']) + def stop(): + """ + Handler for /stop. + Stops TradeThread + """ + msg = self._rpc_stop() + print("msg is", msg) + return msg + + """ + Section to handle configuration and running of the Rest serve + also to check and warn if not bound to 127.0.0.1 as a security risk + """ + + rest_ip = self._config['rest_cmd_line']['listen_ip_address'] + rest_port = self._config['rest_cmd_line']['listen_port'] + + if rest_ip != "127.0.0.1": + i=0 + while i < 10: + logger.info("SECURITY WARNING - Local Rest Server listening to external connections") + logger.info("SECURITY WARNING - This is insecure please set to 127.0.0.1 in config.json") + i += 1 + + # Run the Server + logger.info('Starting Local Rest Server') + app.run(host=rest_ip, port=rest_port) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 928e1798c..c5007970f 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -4,12 +4,20 @@ 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": @@ -20,4 +28,26 @@ if len(argv) == 3 and argv[1] == "daily": 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) + + + diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index ef71ff3a9..21e54f488 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -1,13 +1,10 @@ """ -This module contains class to manage RPC communications (Telegram, Slack, ....) +This module contains class to manage RPC communications (Telegram, Slack, Rest ...) """ -from typing import Any, List import logging -import time - -from freqtrade.rpc.telegram import Telegram -from freqtrade.rpc.local_rest_server import LocalRestSuperWrap +from typing import List +from freqtrade.rpc.rpc import RPC logger = logging.getLogger(__name__) @@ -17,42 +14,29 @@ class RPCManager(object): Class to manage RPC objects (Telegram, Slack, ...) """ def __init__(self, freqtrade) -> None: - """ - Initializes all enabled rpc modules - :param config: config to use - :return: None - """ - self.freqtrade = freqtrade + """ Initializes all enabled rpc modules """ + self.registered_modules: List[RPC] = [] - self.registered_modules: List[str] = [] - self.telegram: Any = None - self._init() - - def _init(self) -> None: - """ - Init RPC modules - :return: - """ - if self.freqtrade.config['telegram'].get('enabled', False): + # Enable telegram + if freqtrade.config['telegram'].get('enabled', False): logger.info('Enabling rpc.telegram ...') - self.registered_modules.append('telegram') - self.telegram = Telegram(self.freqtrade) + from freqtrade.rpc.telegram import Telegram + self.registered_modules.append(Telegram(freqtrade)) - # Added another RPC client - for cmdline local client. - # Uses existing superclass RPC build for Telegram - if self.freqtrade.config['rest_cmd'].get('enabled', False): - self.localRPC = LocalRestSuperWrap(self.freqtrade) - time.sleep(1) + # Enable local rest server for cmd line control + if freqtrade.config['rest_cmd_line'].get('enabled', False): + logger.info('Enabling rpc.local_rest_server ...') + from freqtrade.rpc.local_rest_server import LocalRestSuperWrap + self.registered_modules.append(LocalRestSuperWrap(freqtrade)) def cleanup(self) -> None: - """ - Stops all enabled rpc modules - :return: None - """ - if 'telegram' in self.registered_modules: - logger.info('Cleaning up rpc.telegram ...') - self.registered_modules.remove('telegram') - self.telegram.cleanup() + """ Stops all enabled rpc modules """ + logger.info('Cleaning up rpc modules ...') + while self.registered_modules: + mod = self.registered_modules.pop() + logger.debug('Cleaning up rpc.%s ...', mod.name) + mod.cleanup() + del mod def send_msg(self, msg: str) -> None: """ @@ -60,6 +44,7 @@ class RPCManager(object): :param msg: message :return: None """ - logger.info(msg) - if 'telegram' in self.registered_modules: - self.telegram.send_msg(msg) + logger.info('Sending rpc message: %s', msg) + for mod in self.registered_modules: + logger.debug('Forwarding message to rpc.%s', mod.name) + mod.send_msg(msg) From 9bbc186b1636237189deccac89c4d5b0e3aed19e Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 14 Jun 2018 15:44:12 +0000 Subject: [PATCH 06/31] Updated Stop and start calls added Along with refactoring to base line and use decorators. Also modules loaded optionally if enabled in config or not binds to ip / port set from config.json with warning if not localhost TODO: - use argparse in client, and generally clean client up - create unit test - documentation - extend to other RCP commands, after feedback --- freqtrade/rpc/local_rest_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/local_rest_server.py b/freqtrade/rpc/local_rest_server.py index 24d9a6594..92bb17be8 100644 --- a/freqtrade/rpc/local_rest_server.py +++ b/freqtrade/rpc/local_rest_server.py @@ -95,7 +95,7 @@ class LocalRestSuperWrap(RPC): """ Section to handle configuration and running of the Rest serve - also to check and warn if not bound to 127.0.0.1 as a security risk + also to check and warn if not bound to 127.0.0.1 as a security risk. """ rest_ip = self._config['rest_cmd_line']['listen_ip_address'] From 7caf8a46d485c9be1116658eb58bf89f31b3e082 Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 14 Jun 2018 15:44:52 +0000 Subject: [PATCH 07/31] Updated Stop and start calls added Along with refactoring to base line and use decorators. Also modules loaded optionally if enabled in config or not binds to ip / port set from config.json with warning if not localhost TODO: - use argparse in client, and generally clean client up - create unit test - documentation - extend to other RCP commands, after feedback --- freqtrade/rpc/rpc_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 21e54f488..d796b2454 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -1,5 +1,5 @@ """ -This module contains class to manage RPC communications (Telegram, Slack, Rest ...) +This module contains class to manage RPC communications (Telegram, Slack, Rest ....) """ import logging from typing import List From 40db83c24c803876951f71ed93814398de74923f Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 14 Jun 2018 20:19:15 +0000 Subject: [PATCH 08/31] Added json validation of formats to check IPv4 refactored files and calls to be api_server worked down satisfying review comments of last commit --- freqtrade/configuration.py | 8 +++-- freqtrade/constants.py | 13 +++++++ .../{local_rest_server.py => api_server.py} | 35 ++++++++----------- freqtrade/rpc/rpc_manager.py | 11 +++--- 4 files changed, 39 insertions(+), 28 deletions(-) rename freqtrade/rpc/{local_rest_server.py => api_server.py} (78%) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 2a9e8fbd8..76726b293 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -6,7 +6,7 @@ import json import logging from argparse import Namespace from typing import Optional, Dict, Any -from jsonschema import Draft4Validator, validate +from jsonschema import Draft4Validator, validate, draft4_format_checker from jsonschema.exceptions import ValidationError, best_match import ccxt @@ -209,7 +209,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - validate(conf, constants.CONF_SCHEMA) + validate(conf, constants.CONF_SCHEMA, format_checker=draft4_format_checker) return conf except ValidationError as exception: logger.critical( @@ -217,7 +217,9 @@ class Configuration(object): exception ) raise ValidationError( - best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message + best_match(Draft4Validator(constants.CONF_SCHEMA, + format_checker=draft4_format_checker) + .iter_errors(conf)).message ) def get_config(self) -> Dict[str, Any]: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5be01f977..8560bf8a3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -85,6 +85,19 @@ CONF_SCHEMA = { }, 'required': ['enabled', 'token', 'chat_id'] }, + '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']}, 'internals': { diff --git a/freqtrade/rpc/local_rest_server.py b/freqtrade/rpc/api_server.py similarity index 78% rename from freqtrade/rpc/local_rest_server.py rename to freqtrade/rpc/api_server.py index 92bb17be8..2a1ad0b8c 100644 --- a/freqtrade/rpc/local_rest_server.py +++ b/freqtrade/rpc/api_server.py @@ -6,26 +6,25 @@ from flask import Flask, request from flask_restful import Resource, Api from json import dumps from freqtrade.rpc.rpc import RPC, RPCException +from ipaddress import IPv4Address logger = logging.getLogger(__name__) +app = Flask(__name__) -class LocalRestSuperWrap(RPC): +class ApiServerSuperWrap(RPC): """ - This class is for REST cmd line client + This class is for REST calls across api server """ def __init__(self, freqtrade) -> None: """ - Init the LocalRestServer call, and init the super class RPC + Init the api server, and init the super class RPC :param freqtrade: Instance of a freqtrade bot :return: None """ super().__init__(freqtrade) - """ Constructor - :type interval: int - :param interval: Check interval, in seconds - """ - self.interval = int(1) + + self.interval = 1 thread = threading.Thread(target=self.run, args=(freqtrade,)) # extra comma as ref ! Tuple thread.daemon = True # Daemonize thread @@ -35,11 +34,10 @@ class LocalRestSuperWrap(RPC): def run(self, freqtrade): """ Method that runs forever """ self._config = freqtrade.config - app = Flask(__name__) """ - Define the application routes here - each Telegram command should have a like local substitute + Define the application routes here + each Telegram command should have a like local substitute """ @app.route("/") def hello(): @@ -97,17 +95,14 @@ class LocalRestSuperWrap(RPC): Section to handle configuration and running of the Rest serve also to check and warn if not bound to 127.0.0.1 as a security risk. """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] - rest_ip = self._config['rest_cmd_line']['listen_ip_address'] - rest_port = self._config['rest_cmd_line']['listen_port'] - - if rest_ip != "127.0.0.1": - i=0 - while i < 10: - logger.info("SECURITY WARNING - Local Rest Server listening to external connections") - logger.info("SECURITY WARNING - This is insecure please set to 127.0.0.1 in config.json") - i += 1 + 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') app.run(host=rest_ip, port=rest_port) + diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index d796b2454..9dddcdb80 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -23,11 +23,12 @@ class RPCManager(object): from freqtrade.rpc.telegram import Telegram self.registered_modules.append(Telegram(freqtrade)) - # Enable local rest server for cmd line control - if freqtrade.config['rest_cmd_line'].get('enabled', False): - logger.info('Enabling rpc.local_rest_server ...') - from freqtrade.rpc.local_rest_server import LocalRestSuperWrap - self.registered_modules.append(LocalRestSuperWrap(freqtrade)) + # Enable local rest api server for cmd line control + if freqtrade.config['api_server'].get('enabled', False): + logger.info('Enabling rpc.api_server') + from freqtrade.rpc.api_server import ApiServerSuperWrap + self.registered_modules.append(ApiServerSuperWrap(freqtrade)) + def cleanup(self) -> None: """ Stops all enabled rpc modules """ From 05226f6defc0defee8c2e636727b08529bd40b16 Mon Sep 17 00:00:00 2001 From: creslinux Date: Fri, 15 Jun 2018 09:14:17 +0000 Subject: [PATCH 09/31] Moved from decorators to app.add_url_rule This has the benefit of creating a label which may be helpful if later refactoring. This change misses the main thrust of requests from both Gcarq and Shusso to better layout the code Im running into a challenge with 'self' not being available, or able to be passed in either to decorators or view_func This may simply be how I've instantiated an RPC in the wuperwrap or im within a thread - my very limited exposure to programming is at play! After moving code around lots of ways to no success and google not being helpful im committing for further feeback --- freqtrade/rpc/api_server.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 2a1ad0b8c..04150b4d7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,9 +1,9 @@ import threading import logging -import json +# import json from flask import Flask, request -from flask_restful import Resource, Api +# from flask_restful import Resource, Api from json import dumps from freqtrade.rpc.rpc import RPC, RPCException from ipaddress import IPv4Address @@ -36,10 +36,10 @@ class ApiServerSuperWrap(RPC): self._config = freqtrade.config """ - Define the application routes here + Define the application methods here, called by app.add_url_rule each Telegram command should have a like local substitute """ - @app.route("/") + # @app.route("/") def hello(): # For simple rest server testing via browser # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status @@ -54,7 +54,6 @@ class ApiServerSuperWrap(RPC): '/start' return rest_cmds - @app.route('/daily', methods=['GET']) def daily(): try: timescale = request.args.get('timescale') @@ -71,29 +70,36 @@ class ApiServerSuperWrap(RPC): except RPCException as e: return e - @app.route('/start', methods=['GET']) def start(): """ Handler for /start. Starts TradeThread """ msg = self._rpc_start() - print("msg is", msg) return msg - @app.route('/stop', methods=['GET']) def stop(): """ Handler for /stop. Stops TradeThread """ msg = self._rpc_stop() - print("msg is", msg) return msg + ## defines the url rules available on the api server + ''' + First two arguments passed are /URL and 'Label' + Label can be used as a shortcut when refactoring + ''' + app.add_url_rule('/', 'hello', view_func=hello, methods=['GET']) + app.add_url_rule('/stop', 'stop', view_func=stop, methods=['GET']) + app.add_url_rule('/start', 'start', view_func=start, methods=['GET']) + app.add_url_rule('/daily', 'daily', view_func=daily, methods=['GET']) + + """ - Section to handle configuration and running of the Rest serve - also to check and warn if not bound to 127.0.0.1 as a security risk. + 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'] @@ -104,5 +110,9 @@ class ApiServerSuperWrap(RPC): # Run the Server logger.info('Starting Local Rest Server') - app.run(host=rest_ip, port=rest_port) + try: + app.run(host=rest_ip, port=rest_port) + except: + logger.exception("Api server failed to start, exception message is:") + From 814894da7b9ac813211075cd3be08489d0314c21 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 02:55:27 +0200 Subject: [PATCH 10/31] fix flake8 warnings --- freqtrade/constants.py | 2 +- freqtrade/rpc/api_server.py | 1 + freqtrade/rpc/rest_client.py | 10 ++-------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8560bf8a3..256f1bfe1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -89,7 +89,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'enabled': {'type': 'boolean'}, - 'listen_ip_address': { "format": "ipv4"}, + 'listen_ip_address': {"format": "ipv4"}, 'listen_port': { 'type': 'integer', "minimum": 1024, diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 04150b4d7..9810782ef 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -12,6 +12,7 @@ from ipaddress import IPv4Address logger = logging.getLogger(__name__) app = Flask(__name__) + class ApiServerSuperWrap(RPC): """ This class is for REST calls across api server diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index fa63cf4f7..1e54bbafd 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -8,10 +8,8 @@ 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 +# 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') @@ -47,7 +45,3 @@ if len(argv) == 2 and argv[1] == "stop": time.sleep(2) d = get(get_url).text print(d) - - - - From 8ecfbc2a35fba5a85b8e906cbe846db03e40d78b Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 02:55:49 +0200 Subject: [PATCH 11/31] make rpc modules optional --- freqtrade/rpc/rpc_manager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 9dddcdb80..5e898cd9b 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -18,18 +18,17 @@ class RPCManager(object): self.registered_modules: List[RPC] = [] # Enable telegram - if freqtrade.config['telegram'].get('enabled', False): + if freqtrade.config.get('telegram', {}).get('enabled', False): logger.info('Enabling rpc.telegram ...') from freqtrade.rpc.telegram import Telegram self.registered_modules.append(Telegram(freqtrade)) # Enable local rest api server for cmd line control - if freqtrade.config['api_server'].get('enabled', False): + if freqtrade.config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') from freqtrade.rpc.api_server import ApiServerSuperWrap self.registered_modules.append(ApiServerSuperWrap(freqtrade)) - def cleanup(self) -> None: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') From 556d5073177e5dea3700ba9bb8e6e29dbcb2e9e0 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 02:56:10 +0200 Subject: [PATCH 12/31] move endpoint definitions to class scope --- freqtrade/rpc/api_server.py | 126 +++++++++++++++++------------------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 9810782ef..32d402719 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -25,78 +25,23 @@ class ApiServerSuperWrap(RPC): """ super().__init__(freqtrade) - self.interval = 1 - - thread = threading.Thread(target=self.run, args=(freqtrade,)) # extra comma as ref ! Tuple - thread.daemon = True # Daemonize thread - thread.start() # Start the execution - - - def run(self, freqtrade): - """ Method that runs forever """ self._config = freqtrade.config - """ - Define the application methods here, called by app.add_url_rule - each Telegram command should have a like local substitute - """ - # @app.route("/") - def hello(): - # For simple rest server testing via browser - # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status - # /status /table /performance /count, - # /start /stop /help' + thread = threading.Thread(target=self.run, daemon=True) + thread.start() - rest_cmds ='Commands implemented:
' \ - '/daily?timescale=7' \ - '
' \ - '/stop' \ - '
' \ - '/start' - return rest_cmds + def run(self): + """ Method that runs forever """ - def daily(): - 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'] - ) - - stats = dumps(stats, indent=4, sort_keys=True, default=str) - return stats - except RPCException as e: - return e - - def start(): - """ - Handler for /start. - Starts TradeThread - """ - msg = self._rpc_start() - return msg - - def stop(): - """ - Handler for /stop. - Stops TradeThread - """ - msg = self._rpc_stop() - return msg - - ## defines the url rules available on the api server + # defines the url rules available on the api server ''' First two arguments passed are /URL and 'Label' Label can be used as a shortcut when refactoring ''' - app.add_url_rule('/', 'hello', view_func=hello, methods=['GET']) - app.add_url_rule('/stop', 'stop', view_func=stop, methods=['GET']) - app.add_url_rule('/start', 'start', view_func=start, methods=['GET']) - app.add_url_rule('/daily', 'daily', view_func=daily, methods=['GET']) - + app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + 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']) """ Section to handle configuration and running of the Rest server @@ -105,7 +50,8 @@ class ApiServerSuperWrap(RPC): rest_ip = self._config['api_server']['listen_ip_address'] rest_port = self._config['api_server']['listen_port'] - if not IPv4Address(rest_ip).is_loopback : + 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") @@ -113,7 +59,55 @@ class ApiServerSuperWrap(RPC): logger.info('Starting Local Rest Server') try: app.run(host=rest_ip, port=rest_port) - except: + except Exception: logger.exception("Api server failed to start, exception message is:") + """ + Define the application methods here, called by app.add_url_rule + each Telegram command should have a like local substitute + """ + def hello(self): + # For simple rest server testing via browser + # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status + # /status /table /performance /count, + # /start /stop /help' + rest_cmds = 'Commands implemented:
' \ + '/daily?timescale=7' \ + '
' \ + '/stop' \ + '
' \ + '/start' + return rest_cmds + + def daily(self): + 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'] + ) + + stats = dumps(stats, indent=4, sort_keys=True, default=str) + return stats + except RPCException as e: + return e + + def start(self): + """ + Handler for /start. + Starts TradeThread + """ + msg = self._rpc_start() + return msg + + def stop(self): + """ + Handler for /stop. + Stops TradeThread + """ + msg = self._rpc_stop() + return msg From ada87ba3a02fbbd8f3a89afe13d12a71aaead511 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 03:32:45 +0200 Subject: [PATCH 13/31] convert start, stop and reload_conf to return a dict --- freqtrade/rpc/api_server.py | 6 ++--- freqtrade/rpc/rest_client.py | 2 +- freqtrade/rpc/rpc.py | 29 ++++++++++++++++-------- freqtrade/rpc/telegram.py | 6 ++--- freqtrade/tests/rpc/test_rpc.py | 9 ++++---- freqtrade/tests/rpc/test_rpc_telegram.py | 4 ++-- 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 32d402719..26520949d 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -2,7 +2,7 @@ import threading import logging # import json -from flask import Flask, request +from flask import Flask, request, jsonify # from flask_restful import Resource, Api from json import dumps from freqtrade.rpc.rpc import RPC, RPCException @@ -102,7 +102,7 @@ class ApiServerSuperWrap(RPC): Starts TradeThread """ msg = self._rpc_start() - return msg + return jsonify(msg) def stop(self): """ @@ -110,4 +110,4 @@ class ApiServerSuperWrap(RPC): Stops TradeThread """ msg = self._rpc_stop() - return msg + return jsonify(msg) diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py index 1e54bbafd..cabedebb8 100755 --- a/freqtrade/rpc/rest_client.py +++ b/freqtrade/rpc/rest_client.py @@ -21,7 +21,7 @@ if len(argv) == 1: 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() + d = get(get_url).json() print(d) else: print("\nThe second argument to daily must be an integer, 1,2,3 etc") diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 34802f920..2fd1f9f8b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -27,7 +27,17 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ - pass + def __init__(self, message: str) -> None: + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + def __json__(self): + return { + 'msg': self.message + } class RPC(object): @@ -288,28 +298,27 @@ class RPC(object): value = fiat.convert_amount(total, 'BTC', symbol) return output, total, symbol, value - def _rpc_start(self) -> str: + def _rpc_start(self) -> Dict[str, str]: """ Handler for start """ if self._freqtrade.state == State.RUNNING: - return '*Status:* `already running`' + return {'status': 'already running'} self._freqtrade.state = State.RUNNING - return '`Starting trader ...`' + return {'status': 'starting trader ...'} - def _rpc_stop(self) -> str: + def _rpc_stop(self) -> Dict[str, str]: """ Handler for stop """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED - return '`Stopping trader ...`' + return {'status': 'stopping trader ...'} - return '*Status:* `already stopped`' + return {'status': 'already stopped'} - def _rpc_reload_conf(self) -> str: + def _rpc_reload_conf(self) -> Dict[str, str]: """ Handler for reload_conf. """ self._freqtrade.state = State.RELOAD_CONF - return '*Status:* `Reloading config ...`' + return {'status': 'reloading config ...'} - # FIX: no test for this!!!! def _rpc_forcesell(self, trade_id) -> None: """ Handler for forcesell . diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4dd23971b..a6538a32b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -266,7 +266,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_start() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _stop(self, bot: Bot, update: Update) -> None: @@ -278,7 +278,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_stop() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _reload_conf(self, bot: Bot, update: Update) -> None: @@ -290,7 +290,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_reload_conf() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _forcesell(self, bot: Bot, update: Update) -> None: diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index b49b7fdcb..b788364e1 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -352,11 +352,11 @@ def test_rpc_start(mocker, default_conf) -> None: freqtradebot.state = State.STOPPED result = rpc._rpc_start() - assert '`Starting trader ...`' in result + assert {'status': 'starting trader ...'} == result assert freqtradebot.state == State.RUNNING result = rpc._rpc_start() - assert '*Status:* `already running`' in result + assert {'status': 'already running'} == result assert freqtradebot.state == State.RUNNING @@ -378,11 +378,12 @@ def test_rpc_stop(mocker, default_conf) -> None: freqtradebot.state = State.RUNNING result = rpc._rpc_stop() - assert '`Stopping trader ...`' in result + assert {'status': 'stopping trader ...'} == result assert freqtradebot.state == State.STOPPED result = rpc._rpc_stop() - assert '*Status:* `already stopped`' in result + + assert {'status': 'already stopped'} == result assert freqtradebot.state == State.STOPPED diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index f022c09e4..18e9dc331 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -670,7 +670,7 @@ def test_stop_handle(default_conf, update, mocker) -> None: telegram._stop(bot=MagicMock(), update=update) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 - assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] + assert 'stopping trader' in msg_mock.call_args_list[0][0][0] def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: @@ -718,7 +718,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: telegram._reload_conf(bot=MagicMock(), update=update) assert freqtradebot.state == State.RELOAD_CONF assert msg_mock.call_count == 1 - assert 'Reloading config' in msg_mock.call_args_list[0][0][0] + assert 'reloading config' in msg_mock.call_args_list[0][0][0] def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: From ec4e6272d03cef56b1db1fbf2a02681547684411 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 03:37:19 +0200 Subject: [PATCH 14/31] remove markdown formatting from exception string --- freqtrade/rpc/rpc.py | 22 +++++++++++----------- freqtrade/tests/rpc/test_rpc.py | 12 ++++++------ freqtrade/tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2fd1f9f8b..ac288f161 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -73,9 +73,9 @@ class RPC(object): # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active trade`') + raise RPCException('no active trade') else: result = [] for trade in trades: @@ -119,9 +119,9 @@ class RPC(object): def _rpc_status_table(self) -> DataFrame: trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active order`') + raise RPCException('no active order') else: trades_list = [] for trade in trades: @@ -146,7 +146,7 @@ class RPC(object): profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('*Daily [n]:* `must be an integer greater than 0`') + raise RPCException('timescale must be an integer greater than 0') fiat = self._freqtrade.fiat_converter for day in range(0, timescale): @@ -226,7 +226,7 @@ class RPC(object): .order_by(sql.text('profit_sum DESC')).first() if not best_pair: - raise RPCException('*Status:* `no closed trade`') + raise RPCException('no closed trade') bp_pair, bp_rate = best_pair @@ -291,7 +291,7 @@ class RPC(object): } ) if total == 0.0: - raise RPCException('`All balances are zero.`') + raise RPCException('all balances are zero') fiat = self._freqtrade.fiat_converter symbol = fiat_display_currency @@ -352,7 +352,7 @@ class RPC(object): # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') if trade_id == 'all': # Execute sell for all open orders @@ -369,7 +369,7 @@ class RPC(object): ).first() if not trade: logger.warning('forcesell: Invalid argument received') - raise RPCException('Invalid argument.') + raise RPCException('invalid argument') _exec_forcesell(trade) Trade.session.flush() @@ -380,7 +380,7 @@ class RPC(object): Shows a performance statistic from finished trades """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') pair_rates = Trade.session.query(Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum'), @@ -397,6 +397,6 @@ class RPC(object): def _rpc_count(self) -> List[Trade]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') return Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index b788364e1..864f1601c 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -89,11 +89,11 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_status_table() freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'): + with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table() freqtradebot.create_trade() @@ -415,11 +415,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*Invalid argument.*'): + with pytest.raises(RPCException, match=r'.*invalid argument*'): rpc._rpc_forcesell(None) rpc._rpc_forcesell('all') @@ -430,10 +430,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: rpc._rpc_forcesell('1') freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell('all') freqtradebot.state = State.RUNNING diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 18e9dc331..70b15a3ba 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -596,7 +596,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert '`All balances are zero.`' in result + assert 'all balances are zero' in result def test_start_handle(default_conf, update, mocker) -> None: @@ -871,7 +871,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: update.message.text = '/forcesell' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -879,7 +879,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: update.message.text = '/forcesell 123456' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] def test_performance_handle(default_conf, update, ticker, fee, From 8d54a201226fc7ba93cd34e688982a6acbc3568c Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 03:54:10 +0200 Subject: [PATCH 15/31] return dict from _rpc_status and handle rendering in module impl --- freqtrade/rpc/rpc.py | 48 ++++++++++-------------- freqtrade/rpc/telegram.py | 18 ++++++++- freqtrade/tests/rpc/test_rpc.py | 31 +++++++-------- freqtrade/tests/rpc/test_rpc_telegram.py | 16 +++++++- 4 files changed, 63 insertions(+), 50 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ac288f161..1c2bb926e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -65,7 +65,7 @@ class RPC(object): def send_msg(self, msg: str) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_trade_status(self) -> List[str]: + def _rpc_trade_status(self) -> List[Dict]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function @@ -77,7 +77,7 @@ class RPC(object): elif not trades: raise RPCException('no active trade') else: - result = [] + results = [] for trade in trades: order = None if trade.open_order_id: @@ -88,33 +88,23 @@ class RPC(object): fmt_close_profit = '{:.2f}%'.format( round(trade.close_profit * 100, 2) ) if trade.close_profit else None - message = "*Trade ID:* `{trade_id}`\n" \ - "*Current Pair:* [{pair}]({market_url})\n" \ - "*Open Since:* `{date}`\n" \ - "*Amount:* `{amount}`\n" \ - "*Open Rate:* `{open_rate:.8f}`\n" \ - "*Close Rate:* `{close_rate}`\n" \ - "*Current Rate:* `{current_rate:.8f}`\n" \ - "*Close Profit:* `{close_profit}`\n" \ - "*Current Profit:* `{current_profit:.2f}%`\n" \ - "*Open Order:* `{open_order}`"\ - .format( - trade_id=trade.id, - pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), - open_rate=trade.open_rate, - close_rate=trade.close_rate, - current_rate=current_rate, - amount=round(trade.amount, 8), - close_profit=fmt_close_profit, - current_profit=round(current_profit * 100, 2), - open_order='({} {} rem={:.8f})'.format( - order['type'], order['side'], order['remaining'] - ) if order else None, - ) - result.append(message) - return result + + results.append(dict( + trade_id=trade.id, + pair=trade.pair, + market_url=exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date).humanize(), + open_rate=trade.open_rate, + close_rate=trade.close_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + close_profit=fmt_close_profit, + current_profit=round(current_profit * 100, 2), + open_order='({} {} rem={:.8f})'.format( + order['type'], order['side'], order['remaining'] + ) if order else None, + )) + return results def _rpc_status_table(self) -> DataFrame: trades = Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a6538a32b..c045ffa1d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -136,8 +136,22 @@ class Telegram(RPC): return try: - for trade_msg in self._rpc_trade_status(): - self._send_msg(trade_msg, bot=bot) + results = self._rpc_trade_status() + messages = [ + "*Trade ID:* `{trade_id}`\n" + "*Current Pair:* [{pair}]({market_url})\n" + "*Open Since:* `{date}`\n" + "*Amount:* `{amount}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Close Rate:* `{close_rate}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Profit:* `{close_profit}`\n" + "*Current Profit:* `{current_profit:.2f}%`\n" + "*Open Order:* `{open_order}`".format(**result) + for result in results + ] + for msg in messages: + self._send_msg(msg, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 864f1601c..8bbc75d15 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -51,24 +51,21 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: rpc._rpc_trade_status() freqtradebot.create_trade() - trades = rpc._rpc_trade_status() - trade = trades[0] + results = rpc._rpc_trade_status() - result_message = [ - '*Trade ID:* `1`\n' - '*Current Pair:* ' - '[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' - '*Open Since:* `just now`\n' - '*Amount:* `90.99181074`\n' - '*Open Rate:* `0.00001099`\n' - '*Close Rate:* `None`\n' - '*Current Rate:* `0.00001098`\n' - '*Close Profit:* `None`\n' - '*Current Profit:* `-0.59%`\n' - '*Open Order:* `(limit buy rem=0.00000000)`' - ] - assert trades == result_message - assert trade.find('[ETH/BTC]') >= 0 + assert { + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': 'just now', + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + } == results[0] def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 70b15a3ba..97afe1aac 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -209,7 +209,19 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _rpc_trade_status=MagicMock(return_value=[1, 2, 3]), + _rpc_trade_status=MagicMock(return_value=[{ + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': 'just now', + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + }]), _status_table=status_table, _send_msg=msg_mock ) @@ -223,7 +235,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None: freqtradebot.create_trade() telegram._status(bot=MagicMock(), update=update) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 1 update.message.text = MagicMock() update.message.text.replace = MagicMock(return_value='table 2 3') From a2426c38548e9bf7af330ad96ccba0a7aa104db7 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 22 Jun 2018 04:08:51 +0200 Subject: [PATCH 16/31] refactor _rpc_balance --- freqtrade/rpc/rpc.py | 27 +++++++++++++++------------ freqtrade/rpc/telegram.py | 9 ++++----- freqtrade/tests/rpc/test_rpc.py | 21 +++++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1c2bb926e..f3b825c79 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -5,7 +5,7 @@ import logging from abc import abstractmethod from datetime import datetime, timedelta, date from decimal import Decimal -from typing import Dict, Tuple, Any, List +from typing import Dict, Any, List import arrow import sqlalchemy as sql @@ -254,7 +254,7 @@ class RPC(object): 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]: + def _rpc_balance(self, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 @@ -271,22 +271,25 @@ class RPC(object): rate = exchange.get_ticker(coin + '/BTC', False)['bid'] est_btc: float = rate * balance['total'] total = total + est_btc - output.append( - { - 'currency': coin, - 'available': balance['free'], - 'balance': balance['total'], - 'pending': balance['used'], - 'est_btc': est_btc - } - ) + output.append({ + 'currency': coin, + 'available': balance['free'], + 'balance': balance['total'], + 'pending': balance['used'], + 'est_btc': est_btc, + }) if total == 0.0: raise RPCException('all balances are zero') fiat = self._freqtrade.fiat_converter symbol = fiat_display_currency value = fiat.convert_amount(total, 'BTC', symbol) - return output, total, symbol, value + return { + 'currencies': output, + 'total': total, + 'symbol': symbol, + 'value': value, + } def _rpc_start(self) -> Dict[str, str]: """ Handler for start """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c045ffa1d..1d186f9df 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -253,10 +253,9 @@ class Telegram(RPC): def _balance(self, bot: Bot, update: Update) -> None: """ Handler for /balance """ try: - currencys, total, symbol, value = \ - self._rpc_balance(self._config['fiat_display_currency']) + result = self._rpc_balance(self._config['fiat_display_currency']) output = '' - for currency in currencys: + for currency in result['currencies']: output += "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ @@ -264,8 +263,8 @@ class Telegram(RPC): "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) output += "\n*Estimated Value*:\n" \ - "\t`BTC: {0: .8f}`\n" \ - "\t`{1}: {2: .2f}`\n".format(total, symbol, value) + "\t`BTC: {total: .8f}`\n" \ + "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 8bbc75d15..2092f02b6 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -319,16 +319,17 @@ def test_rpc_balance_handle(default_conf, mocker): freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) - output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(total, 12) - assert prec_satoshi(value, 180000) - assert 'USD' in symbol - assert len(output) == 1 - assert 'BTC' in output[0]['currency'] - assert prec_satoshi(output[0]['available'], 10) - assert prec_satoshi(output[0]['balance'], 12) - assert prec_satoshi(output[0]['pending'], 2) - assert prec_satoshi(output[0]['est_btc'], 12) + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12) + assert prec_satoshi(result['value'], 180000) + assert 'USD' == result['symbol'] + assert result['currencies'] == [{ + 'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }] def test_rpc_start(mocker, default_conf) -> None: From 4aab55d27bd787d056050056f92e114ee1c47976 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 09:27:28 +0200 Subject: [PATCH 17/31] api_server: fix flake8 warnings and implement missing methods --- freqtrade/rpc/api_server.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 26520949d..8927584fe 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -53,7 +53,8 @@ class ApiServerSuperWrap(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, e.g 127.0.0.1 in config.json") + 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') @@ -62,10 +63,24 @@ class ApiServerSuperWrap(RPC): except Exception: logger.exception("Api server failed to start, exception message is:") + def cleanup(self) -> None: + # TODO: implement me + raise NotImplementedError + + @property + def name(self) -> str: + # TODO: implement me + raise NotImplementedError + + def send_msg(self, msg: str) -> None: + # TODO: implement me + raise NotImplementedError + """ Define the application methods here, called by app.add_url_rule each Telegram command should have a like local substitute """ + def hello(self): # For simple rest server testing via browser # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status From 77d53b0b7cdb755899aefe33554db9fd467bb4b2 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 09:28:13 +0200 Subject: [PATCH 18/31] remove _rpc_status_table and reuse _rpc_status instead --- freqtrade/rpc/rpc.py | 27 +------------ freqtrade/rpc/telegram.py | 48 +++++++++++++++++------- freqtrade/tests/rpc/test_rpc.py | 39 +++---------------- freqtrade/tests/rpc/test_rpc_telegram.py | 9 +++-- 4 files changed, 46 insertions(+), 77 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f3b825c79..26a55542f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -10,10 +10,8 @@ from typing import Dict, Any, List import arrow import sqlalchemy as sql from numpy import mean, nan_to_num -from pandas import DataFrame from freqtrade import exchange -from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.state import State @@ -93,7 +91,7 @@ class RPC(object): trade_id=trade.id, pair=trade.pair, market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), + open_date=arrow.get(trade.open_date), open_rate=trade.open_rate, close_rate=trade.close_rate, current_rate=current_rate, @@ -106,29 +104,6 @@ class RPC(object): )) return results - def _rpc_status_table(self) -> DataFrame: - trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') - elif not trades: - raise RPCException('no active order') - else: - trades_list = [] - for trade in trades: - # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - trades_list.append([ - trade.id, - trade.pair, - shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), - '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) - ]) - - columns = ['ID', 'Pair', 'Since', 'Profit'] - df_statuses = DataFrame.from_records(trades_list, columns=columns) - df_statuses = df_statuses.set_index(columns[0]) - return df_statuses - def _rpc_daily_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> List[List[Any]]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1d186f9df..90913b4a5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,12 +6,15 @@ This module manage Telegram communication import logging from typing import Any, Callable +import arrow +from pandas import DataFrame from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater from freqtrade.__init__ import __version__ +from freqtrade.misc import shorten_date from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -137,21 +140,25 @@ class Telegram(RPC): try: results = self._rpc_trade_status() - messages = [ - "*Trade ID:* `{trade_id}`\n" - "*Current Pair:* [{pair}]({market_url})\n" - "*Open Since:* `{date}`\n" - "*Amount:* `{amount}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Close Rate:* `{close_rate}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Profit:* `{close_profit}`\n" - "*Current Profit:* `{current_profit:.2f}%`\n" - "*Open Order:* `{open_order}`".format(**result) - for result in results - ] + messages = [] + for result in results: + result['open_date'] = arrow.get(result['open_date']).humanize() + messages.append( + "*Trade ID:* `{trade_id}`\n" + "*Current Pair:* [{pair}]({market_url})\n" + "*Open Since:* `{open_date}`\n" + "*Amount:* `{amount}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Close Rate:* `{close_rate}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Profit:* `{close_profit}`\n" + "*Current Profit:* `{current_profit:.2f}%`\n" + "*Open Order:* `{open_order}`".format(**result) + ) + for msg in messages: self._send_msg(msg, bot=bot) + except RPCException as e: self._send_msg(str(e), bot=bot) @@ -165,7 +172,20 @@ class Telegram(RPC): :return: None """ try: - df_statuses = self._rpc_status_table() + + results = self._rpc_trade_status() + data = [ + [ + result['trade_id'], + result['pair'], + shorten_date(arrow.get(result['open_date']).humanize(only_distance=True)), + result['current_profit'], + ] for result in results + ] + columns = ['ID', 'Pair', 'Since', 'Profit'] + df_statuses = DataFrame.from_records(data, columns=columns) + df_statuses = df_statuses.set_index(columns[0]) + message = tabulate(df_statuses, headers='keys', tablefmt='simple') self._send_msg("
{}
".format(message), parse_mode=ParseMode.HTML) except RPCException as e: diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 2092f02b6..dc2a262a6 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -7,6 +7,7 @@ Unit test file for rpc/rpc.py from datetime import datetime from unittest.mock import MagicMock +import arrow import pytest from freqtrade.freqtradebot import FreqtradeBot @@ -39,6 +40,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: get_fee=fee ) + now = arrow.utcnow() + now_mock = mocker.patch('freqtrade.freqtradebot.datetime', MagicMock()) + now_mock.utcnow = lambda: now.datetime + freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) @@ -57,7 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'trade_id': 1, 'pair': 'ETH/BTC', 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', - 'date': 'just now', + 'open_date': now, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, @@ -68,38 +73,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } == results[0] -def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: - """ - Test rpc_status_table() method - """ - patch_get_signal(mocker, (True, False)) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - get_fee=fee - ) - - freqtradebot = FreqtradeBot(default_conf) - rpc = RPC(freqtradebot) - - freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*trader is not running*'): - rpc._rpc_status_table() - - freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*no active order*'): - rpc._rpc_status_table() - - freqtradebot.create_trade() - result = rpc._rpc_status_table() - assert 'just now' in result['Since'].all() - assert 'ETH/BTC' in result['Pair'].all() - assert '-0.59%' in result['Profit'].all() - - def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: """ diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 97afe1aac..6bf76ce8a 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -213,7 +213,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None: 'trade_id': 1, 'pair': 'ETH/BTC', 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', - 'date': 'just now', + 'open_date': datetime.utcnow(), 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, @@ -289,7 +289,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0] -def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: +def test_status_table_handle(default_conf, limit_buy_order, update, ticker, fee, mocker) -> None: """ Test _status_table() method """ @@ -299,7 +299,8 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), get_ticker=ticker, - buy=MagicMock(return_value={'id': 'mocked_order_id'}), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) msg_mock = MagicMock() @@ -324,7 +325,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'no active order' in msg_mock.call_args_list[0][0][0] + assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data From 72b89e694ae714cf2c6852c299e7a3e2172b18d2 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 09:29:45 +0200 Subject: [PATCH 19/31] telegram: remove duplicate test for /status --- freqtrade/tests/rpc/test_rpc_telegram.py | 57 ------------------------ 1 file changed, 57 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 6bf76ce8a..e833af2b3 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -186,63 +186,6 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: ) -def test_status(default_conf, update, mocker, fee, ticker) -> None: - """ - Test _status() method - """ - update.message.chat.id = 123 - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - conf['telegram']['chat_id'] = 123 - - patch_get_signal(mocker, (True, False)) - patch_coinmarketcap(mocker) - mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - get_pair_detail_url=MagicMock(), - get_fee=fee, - ) - msg_mock = MagicMock() - status_table = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _rpc_trade_status=MagicMock(return_value=[{ - 'trade_id': 1, - 'pair': 'ETH/BTC', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', - 'open_date': datetime.utcnow(), - 'open_rate': 1.099e-05, - 'close_rate': None, - 'current_rate': 1.098e-05, - 'amount': 90.99181074, - 'close_profit': None, - 'current_profit': -0.59, - 'open_order': '(limit buy rem=0.00000000)' - }]), - _status_table=status_table, - _send_msg=msg_mock - ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - - freqtradebot = FreqtradeBot(conf) - telegram = Telegram(freqtradebot) - - # Create some test data - for _ in range(3): - freqtradebot.create_trade() - - telegram._status(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - - update.message.text = MagicMock() - update.message.text.replace = MagicMock(return_value='table 2 3') - telegram._status(bot=MagicMock(), update=update) - assert status_table.call_count == 1 - - def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: """ Test _status() method From d0d128d20304bac9982b9f901c3f12e7ca7c1654 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 23 Jun 2018 09:08:39 +0000 Subject: [PATCH 20/31] Moved registering application urls out of the run def and into their own Added 404 handling Split registration of URLs that use rpc.rpc and others into own def. Seems logical to be able to register separately for later use. --- freqtrade/rpc/api_server.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8927584fe..945e870bc 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -27,13 +27,19 @@ class ApiServerSuperWrap(RPC): 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 run(self): - """ Method that runs forever """ + def register_rest_other(self): + #Added as a placeholder for app URLs that are not implemented in rpc.rpc + app.register_error_handler(404, self.page_not_found) - # defines the url rules available on the api server + def register_rest_rpc_urls(self): + # register the url rules available on the api server ''' First two arguments passed are /URL and 'Label' Label can be used as a shortcut when refactoring @@ -43,6 +49,9 @@ class ApiServerSuperWrap(RPC): app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) + def run(self): + """ Method that runs 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. @@ -81,6 +90,12 @@ class ApiServerSuperWrap(RPC): each Telegram command should have a like local substitute """ + 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): # For simple rest server testing via browser # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status From 5cda78bf324838979b43b452ce7ba83cffd26d03 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 23 Jun 2018 09:19:34 +0000 Subject: [PATCH 21/31] moved default page "/" index into self_register_other() out of the block of URLs that call rpc.rcp functionality. --- freqtrade/rpc/api_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 945e870bc..7c8d33231 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -37,14 +37,16 @@ class ApiServerSuperWrap(RPC): def register_rest_other(self): #Added as a placeholder for app URLs that are not implemented in rpc.rpc 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): # register the url rules available on the api server + # This is where to register rest urls that make use of + # rpc.rpc functions ''' First two arguments passed are /URL and 'Label' Label can be used as a shortcut when refactoring ''' - app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) 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']) From 6c3ea7c5f92631412fe46d0b2d0350cf019386d8 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 23 Jun 2018 09:22:25 +0000 Subject: [PATCH 22/31] flake 8 fix --- 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 7c8d33231..3b51126a9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -35,7 +35,7 @@ class ApiServerSuperWrap(RPC): thread.start() def register_rest_other(self): - #Added as a placeholder for app URLs that are not implemented in rpc.rpc + # Added as a placeholder for app URLs that are not implemented in rpc.rpc app.register_error_handler(404, self.page_not_found) app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) From c72397936edc526423edbd138d6f081f5b71b117 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 23 Jun 2018 09:48:51 +0000 Subject: [PATCH 23/31] Updated def comments to be __docstring__ compatible --- freqtrade/rpc/api_server.py | 40 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3b51126a9..3a8391b98 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -35,24 +35,27 @@ class ApiServerSuperWrap(RPC): thread.start() def register_rest_other(self): - # Added as a placeholder for app URLs that are not implemented in rpc.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']) def register_rest_rpc_urls(self): - # register the url rules available on the api server - # This is where to register rest urls that make use of - # rpc.rpc functions - ''' + """ + 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']) def run(self): - """ Method that runs forever """ + """ Method that runs flask app in its own thread forever """ """ Section to handle configuration and running of the Rest server @@ -93,17 +96,19 @@ class ApiServerSuperWrap(RPC): """ def page_not_found(self, error): - # return "404 not found", 404 + # 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): - # For simple rest server testing via browser - # cmds = 'Try uri:/daily?timescale=7 /profit /balance /status - # /status /table /performance /count, - # /start /stop /help' + """ + 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:
' \ '/daily?timescale=7' \ '
' \ @@ -113,6 +118,11 @@ class ApiServerSuperWrap(RPC): 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") @@ -131,7 +141,8 @@ class ApiServerSuperWrap(RPC): def start(self): """ Handler for /start. - Starts TradeThread + + Starts TradeThread in bot if stopped. """ msg = self._rpc_start() return jsonify(msg) @@ -139,7 +150,8 @@ class ApiServerSuperWrap(RPC): def stop(self): """ Handler for /stop. - Stops TradeThread + + Stops TradeThread in bot if running """ msg = self._rpc_stop() return jsonify(msg) From 0f6c41389ca593b1a1931b01fb7511699f90c827 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 12:14:38 +0200 Subject: [PATCH 24/31] rename ApiServerSuperWrap into ApiServer --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc_manager.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3a8391b98..220485141 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) app = Flask(__name__) -class ApiServerSuperWrap(RPC): +class ApiServer(RPC): """ This class is for REST calls across api server """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 5e898cd9b..c3433fe56 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -26,8 +26,8 @@ class RPCManager(object): # 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 ApiServerSuperWrap - self.registered_modules.append(ApiServerSuperWrap(freqtrade)) + from freqtrade.rpc.api_server import ApiServer + self.registered_modules.append(ApiServer(freqtrade)) def cleanup(self) -> None: """ Stops all enabled rpc modules """ From d0b729e78e45cbe9d7ceee2a39e6deee28d4bf03 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 12:28:32 +0200 Subject: [PATCH 25/31] implement name property in abstract class --- freqtrade/rpc/api_server.py | 11 ++--------- freqtrade/rpc/rpc.py | 10 +++++----- freqtrade/rpc/telegram.py | 4 ---- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 220485141..bfde304bd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -78,17 +78,10 @@ class ApiServer(RPC): logger.exception("Api server failed to start, exception message is:") def cleanup(self) -> None: - # TODO: implement me - raise NotImplementedError - - @property - def name(self) -> str: - # TODO: implement me - raise NotImplementedError + pass def send_msg(self, msg: str) -> None: - # TODO: implement me - raise NotImplementedError + pass """ Define the application methods here, called by app.add_url_rule diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e86af8a11..c31d46878 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -49,15 +49,15 @@ class RPC(object): """ self._freqtrade = freqtrade + @property + def name(self) -> str: + """ Returns the lowercase name of the implementation """ + return self.__class__.__name__.lower() + @abstractmethod def cleanup(self) -> None: """ Cleanup pending module resources """ - @property - @abstractmethod - def name(self) -> str: - """ Returns the lowercase name of this module """ - @abstractmethod def send_msg(self, msg: str) -> None: """ Sends a message to all registered rpc modules """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 90913b4a5..add9968dc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -58,10 +58,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call class Telegram(RPC): """ This class handles all telegram communication """ - @property - def name(self) -> str: - return "telegram" - def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC From a0c9a9585d23463ef9c88903355bb911b34b2522 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 12:52:29 +0200 Subject: [PATCH 26/31] apiserver: implement tests for start() and stop() --- freqtrade/rpc/api_server.py | 5 ++- freqtrade/tests/conftest.py | 9 ++++ freqtrade/tests/rpc/test_rpc_apiserver.py | 52 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 freqtrade/tests/rpc/test_rpc_apiserver.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index bfde304bd..be9f8a6fd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,3 +1,4 @@ +import json import threading import logging # import json @@ -138,7 +139,7 @@ class ApiServer(RPC): Starts TradeThread in bot if stopped. """ msg = self._rpc_start() - return jsonify(msg) + return json.dumps(msg) def stop(self): """ @@ -147,4 +148,4 @@ class ApiServer(RPC): Stops TradeThread in bot if running """ msg = self._rpc_stop() - return jsonify(msg) + return json.dumps(msg) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index ce22cd193..25a9b8d00 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -83,6 +83,15 @@ def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> Non ) +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 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 3384679bad36a5b199abe39b54d3b5a080da2095 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Sat, 23 Jun 2018 09:38:20 -0500 Subject: [PATCH 27/31] bump develop to 0.17.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 7cf0fa996..ac00264f0 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.17.0' +__version__ = '0.17.1' class DependencyException(BaseException): From 5e7e977ffab0b4f60f31ade8087bb038ec7c5ca6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 24 Jun 2018 14:23:05 +0200 Subject: [PATCH 28/31] Update ccxt from 1.14.256 to 1.14.257 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51312791e..437bb5533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.256 +ccxt==1.14.257 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 From 43f1a1d264765f106ad9f94b5fd1f3ffb70d20de Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sun, 24 Jun 2018 19:52:12 +0200 Subject: [PATCH 29/31] rework download_backtest script --- freqtrade/arguments.py | 7 +++++ scripts/download_backtest_data.py | 48 ++++++++++++++++--------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 31232f1ff..8ce7c546d 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -334,3 +334,10 @@ class Arguments(object): nargs='+', dest='timeframes', ) + + self.parser.add_argument( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes', + dest='erase', + action='store_true' + ) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 2f76c1232..686098f94 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -3,11 +3,14 @@ """This script generate json data from bittrex""" import json import sys -import os +from pathlib import Path import arrow -from freqtrade import (arguments, misc) +from freqtrade import arguments +from freqtrade.arguments import TimeRange from freqtrade.exchange import Exchange +from freqtrade.optimize import download_backtesting_testdata + DEFAULT_DL_PATH = 'user_data/data' @@ -17,25 +20,27 @@ args = arguments.parse_args() timeframes = args.timeframes -dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange) +dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange) if args.export: - dl_path = args.export + dl_path = Path(args.export) -if not os.path.isdir(dl_path): +if not dl_path.is_dir(): sys.exit(f'Directory {dl_path} does not exist.') -pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json') -if not os.path.isfile(pairs_file): +pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json') +if not pairs_file.exists(): sys.exit(f'No pairs file found with path {pairs_file}.') -with open(pairs_file) as file: +with pairs_file.open() as file: PAIRS = list(set(json.load(file))) PAIRS.sort() -since_time = None + +timerange = TimeRange() if args.days: - since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 + time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d") + timerange = arguments.parse_timerange(f'{time_since}-') print(f'About to download pairs: {PAIRS} to {dl_path}') @@ -59,21 +64,18 @@ for pair in PAIRS: print(f"skipping pair {pair}") continue for tick_interval in timeframes: - print(f'downloading pair {pair}, interval {tick_interval}') - - data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time) - if not data: - print('\tNo data was downloaded') - break - - print('\tData was downloaded for period %s - %s' % ( - arrow.get(data[0][0] / 1000).format(), - arrow.get(data[-1][0] / 1000).format())) - - # save data pair_print = pair.replace('/', '_') filename = f'{pair_print}-{tick_interval}.json' - misc.file_dump_json(os.path.join(dl_path, filename), data) + dl_file = dl_path.joinpath(filename) + if args.erase and dl_file.exists(): + print(f'Deleting existing data for pair {pair}, interval {tick_interval}') + dl_file.unlink() + + print(f'downloading pair {pair}, interval {tick_interval}') + download_backtesting_testdata(str(dl_path), exchange=exchange, + pair=pair, + tick_interval=tick_interval, + timerange=timerange) if pairs_not_available: From 127e50c932092717744e7e558c41d26807c8e4d2 Mon Sep 17 00:00:00 2001 From: gcarq Date: Mon, 25 Jun 2018 00:04:27 +0200 Subject: [PATCH 30/31] use dict as argument for rpc.send_msg --- freqtrade/freqtradebot.py | 35 ++++++++++------ freqtrade/main.py | 12 +++--- freqtrade/rpc/api_server.py | 3 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/rpc_manager.py | 13 +++--- freqtrade/rpc/telegram.py | 6 +-- freqtrade/tests/rpc/test_rpc_manager.py | 8 ++-- freqtrade/tests/rpc/test_rpc_telegram.py | 32 ++++++++------- freqtrade/tests/test_freqtradebot.py | 52 +++++++++++++----------- 9 files changed, 91 insertions(+), 72 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e25ed66cf..c00aa68f5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -93,7 +93,9 @@ class FreqtradeBot(object): # Log state transition state = self.state if state != old_state: - self.rpc.send_msg(f'*Status:* `{state.name.lower()}`') + self.rpc.send_msg({ + 'status': f'{state.name.lower()}' + }) logger.info('Changing state to: %s', state.name) if state == State.STOPPED: @@ -169,9 +171,9 @@ class FreqtradeBot(object): except OperationalException: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg( - f'*Status:* OperationalException:\n```\n{tb}```{hint}' - ) + self.rpc.send_msg({ + 'status': f'OperationalException:\n```\n{tb}```{hint}' + }) logger.exception('OperationalException. Stopping trader ...') self.state = State.STOPPED return state_changed @@ -356,11 +358,12 @@ class FreqtradeBot(object): ) # Create trade entity and return - self.rpc.send_msg( - f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \ -with limit `{buy_limit:.8f} ({stake_amount:.6f} \ -{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" - ) + self.rpc.send_msg({ + 'status': + f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \ + with limit `{buy_limit:.8f} ({stake_amount:.6f} \ + {stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" + }) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -540,7 +543,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ Trade.session.delete(trade) Trade.session.flush() logger.info('Buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'status': f'Unfilled buy order for {pair_s} cancelled due to timeout' + }) return True # if trade is partially complete, edit the stake details for the trade @@ -549,7 +554,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.stake_amount = trade.amount * trade.open_rate trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'status': f'Remaining buy order for {pair_s} cancelled due to timeout' + }) return False # FIX: 20180110, should cancel_order() be cond. or unconditionally called? @@ -567,7 +574,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.close_date = None trade.is_open = True trade.open_order_id = None - self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled') + self.rpc.send_msg({ + 'status': f'Unfilled sell order for {pair_s} cancelled due to timeout' + }) logger.info('Sell order timeout for %s.', trade) return True @@ -627,5 +636,5 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ ) # Send the message - self.rpc.send_msg(message) + self.rpc.send_msg({'status': message}) Trade.session.flush() diff --git a/freqtrade/main.py b/freqtrade/main.py index 9d17a403a..74d8da031 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -59,7 +59,9 @@ def main(sysargv: List[str]) -> None: logger.exception('Fatal exception!') finally: if freqtrade: - freqtrade.rpc.send_msg('*Status:* `Process died ...`') + freqtrade.rpc.send_msg({ + 'status': 'process died' + }) freqtrade.cleanup() sys.exit(return_code) @@ -73,11 +75,9 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: # Create new instance freqtrade = FreqtradeBot(Configuration(args).get_config()) - freqtrade.rpc.send_msg( - '*Status:* `Config reloaded ...`'.format( - freqtrade.state.name.lower() - ) - ) + freqtrade.rpc.send_msg({ + 'status': 'config reloaded' + }) return freqtrade diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be9f8a6fd..7a006b202 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -2,6 +2,7 @@ import json import threading import logging # import json +from typing import Dict from flask import Flask, request, jsonify # from flask_restful import Resource, Api @@ -81,7 +82,7 @@ class ApiServer(RPC): def cleanup(self) -> None: pass - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: pass """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c31d46878..754c511d4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -59,7 +59,7 @@ class RPC(object): """ Cleanup pending module resources """ @abstractmethod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ def _rpc_trade_status(self) -> List[Dict]: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c3433fe56..ebefeadb2 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,7 +2,7 @@ This module contains class to manage RPC communications (Telegram, Slack, Rest ....) """ import logging -from typing import List +from typing import List, Dict from freqtrade.rpc.rpc import RPC @@ -38,11 +38,14 @@ class RPCManager(object): mod.cleanup() del mod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: """ - Send given markdown message to all registered rpc modules - :param msg: message - :return: None + Send given message to all registered rpc modules. + A message consists of one or more key value pairs of strings. + e.g.: + { + 'status': 'stopping bot' + } """ logger.info('Sending rpc message: %s', msg) for mod in self.registered_modules: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index add9968dc..3dd0d34c7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,7 @@ This module manage Telegram communication """ import logging -from typing import Any, Callable +from typing import Any, Callable, Dict import arrow from pandas import DataFrame @@ -113,9 +113,9 @@ class Telegram(RPC): """ self._updater.stop() - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: """ Send a message to telegram channel """ - self._send_msg(msg) + self._send_msg('*Status:* `{status}`'.format(**msg)) @authorized_only def _status(self, bot: Bot, update: Update) -> None: diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 805424d26..f9c5de035 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -102,9 +102,9 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({'status': 'test'}) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'status': 'test'}", caplog.record_tuples) assert telegram_mock.call_count == 0 @@ -117,7 +117,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({'status': 'test'}) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'status': 'test'}", caplog.record_tuples) assert telegram_mock.call_count == 1 diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index bcf7c8e32..5d24bd82c 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -700,12 +700,13 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] - assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] + last_call = rpc_mock.call_args_list[-1][0][0]['status'] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert 'Amount' in last_call + assert '0.00001172' in last_call + assert 'profit: 6.11%, 0.00006126' in last_call + assert '0.919 USD' in last_call def test_forcesell_down_handle(default_conf, update, ticker, fee, @@ -745,13 +746,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, update.message.text = '/forcesell 1' telegram._forcesell(bot=MagicMock(), update=update) + last_call = rpc_mock.call_args_list[-1][0][0]['status'] assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] - assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert 'Amount' in last_call + assert '0.00001044' in last_call + assert 'loss: -5.48%, -0.00005492' in last_call + assert '-0.824 USD' in last_call def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: @@ -785,9 +787,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker assert rpc_mock.call_count == 4 for args in rpc_mock.call_args_list: - assert '0.00001098' in args[0][0] - assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0] - assert '-0.089 USD' in args[0][0] + assert '0.00001098' in args[0][0]['status'] + assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]['status'] + assert '-0.089 USD' in args[0][0]['status'] def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 1cb2bfca2..1dc035a46 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -725,7 +725,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> result = freqtrade._process() assert result is False assert freqtrade.state == State.STOPPED - assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] + assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] def test_process_trade_handling( @@ -1345,13 +1345,14 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert 'Profit' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] - assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] + last_call = rpc_mock.call_args_list[-1][0][0]['status'] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert 'Amount' in last_call + assert 'Profit' in last_call + assert '0.00001172' in last_call + assert 'profit: 6.11%, 0.00006126' in last_call + assert '0.919 USD' in last_call def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: @@ -1387,12 +1388,13 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] - assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] + last_call = rpc_mock.call_args_list[-1][0][0]['status'] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert 'Amount' in last_call + assert '0.00001044' in last_call + assert 'loss: -5.48%, -0.00005492' in last_call + assert '-0.824 USD' in last_call def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, @@ -1429,12 +1431,13 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0] - assert 'USD' not in rpc_mock.call_args_list[-1][0][0] + last_call = rpc_mock.call_args_list[-1][0][0]['status'] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert 'Amount' in last_call + assert '0.00001172' in last_call + assert '(profit: 6.11%, 0.00006126)' in last_call + assert 'USD' not in last_call def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, @@ -1471,10 +1474,11 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] + last_call = rpc_mock.call_args_list[-1][0][0]['status'] + assert 'Selling' in last_call + assert '[ETH/BTC]' in last_call + assert '0.00001044' in last_call + assert 'loss: -5.48%, -0.00005492' in last_call def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, From d272e3b666482ee66828cf226b33a93585bd2cd7 Mon Sep 17 00:00:00 2001 From: gcarq Date: Mon, 25 Jun 2018 00:07:53 +0200 Subject: [PATCH 31/31] remove flask-jsonify in favor of json.dumps --- freqtrade/rpc/api_server.py | 14 +++++++++----- requirements.txt | 1 - 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 7a006b202..af1d76c87 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, jsonify +from flask import Flask, request # from flask_restful import Resource, Api from json import dumps from freqtrade.rpc.rpc import RPC, RPCException @@ -91,10 +91,14 @@ class ApiServer(RPC): """ 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 + """ + 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): """ diff --git a/requirements.txt b/requirements.txt index 33fbd1523..fbc478607 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,5 +26,4 @@ coinmarketcap==5.0.3 #Added for local rest client Flask==1.0.2 -flask-jsonpify==1.5.0 flask-restful==0.3.6