diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 7c3a5eb4b..c994eb8c2 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 @@ -202,7 +202,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( @@ -210,7 +210,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/api_server.py b/freqtrade/rpc/api_server.py new file mode 100644 index 000000000..04150b4d7 --- /dev/null +++ b/freqtrade/rpc/api_server.py @@ -0,0 +1,118 @@ +import threading +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, RPCException +from ipaddress import IPv4Address + + +logger = logging.getLogger(__name__) +app = Flask(__name__) + +class ApiServerSuperWrap(RPC): + """ + This class is for REST calls across api server + """ + def __init__(self, freqtrade) -> None: + """ + Init the api server, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + + self.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' + + rest_cmds ='Commands implemented:
' \ + '/daily?timescale=7' \ + '
' \ + '/stop' \ + '
' \ + '/start' + return rest_cmds + + 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 + ''' + 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 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'] + + if not IPv4Address(rest_ip).is_loopback : + logger.info("SECURITY WARNING - Local Rest Server listening to external connections") + logger.info("SECURITY WARNING - This is insecure please set to your loopback, e.g 127.0.0.1 in config.json") + + # Run the Server + logger.info('Starting Local Rest Server') + try: + app.run(host=rest_ip, port=rest_port) + except: + logger.exception("Api server failed to start, exception message is:") + + diff --git a/freqtrade/rpc/rest_client.py b/freqtrade/rpc/rest_client.py new file mode 100755 index 000000000..fa63cf4f7 --- /dev/null +++ b/freqtrade/rpc/rest_client.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Simple command line client into RPC commands +Can be used as an alternate to Telegram +""" + +import time +from requests import get +from sys import argv + + + +#TODO - use argparse to clean this up +#TODO - use IP and Port from config.json not hardcode + +if len(argv) == 1: + print('\nThis script accepts the following arguments') + print('- daily (int) - Where int is the number of days to report back. daily 3') + print('- start - this will start the trading thread') + print('- stop - this will start the trading thread') + print('- there will be more....\n') + +if len(argv) == 3 and argv[1] == "daily": + if str.isnumeric(argv[2]): + get_url = 'http://localhost:5002/daily?timescale=' + argv[2] + d=get(get_url).json() + print(d) + else: + print("\nThe second argument to daily must be an integer, 1,2,3 etc") + +if len(argv) == 2 and argv[1] == "start": + get_url = 'http://localhost:5002/start' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) + +if len(argv) == 2 and argv[1] == "stop": + get_url = 'http://localhost:5002/stop' + d = get(get_url).text + print(d) + + if "already" not in d: + time.sleep(2) + d = get(get_url).text + print(d) + + + + diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 252bbcdd8..9dddcdb80 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, Rest ....) """ import logging from typing import List @@ -23,6 +23,13 @@ class RPCManager(object): 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): + 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 ...') diff --git a/requirements.txt b/requirements.txt index 41e246d50..3c9fc5825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,8 @@ coinmarketcap==5.0.3 # Required for plotting data #plotly==2.3.0 + +#Added for local rest client +Flask==1.0.2 +flask-jsonpify==1.5.0 +flask-restful==0.3.6