Merge branch 'flask_rest' of https://github.com/creslinux/freqtrade into feature/flask-rest

This commit is contained in:
gcarq 2018-06-22 02:43:10 +02:00
commit 089f633c06
6 changed files with 202 additions and 4 deletions

View File

@ -6,7 +6,7 @@ import json
import logging import logging
from argparse import Namespace from argparse import Namespace
from typing import Optional, Dict, Any 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 from jsonschema.exceptions import ValidationError, best_match
import ccxt import ccxt
@ -202,7 +202,7 @@ class Configuration(object):
:return: Returns the config if valid, otherwise throw an exception :return: Returns the config if valid, otherwise throw an exception
""" """
try: try:
validate(conf, constants.CONF_SCHEMA) validate(conf, constants.CONF_SCHEMA, format_checker=draft4_format_checker)
return conf return conf
except ValidationError as exception: except ValidationError as exception:
logger.critical( logger.critical(
@ -210,7 +210,9 @@ class Configuration(object):
exception exception
) )
raise ValidationError( 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]: def get_config(self) -> Dict[str, Any]:

View File

@ -85,6 +85,19 @@ CONF_SCHEMA = {
}, },
'required': ['enabled', 'token', 'chat_id'] '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'}, 'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': { 'internals': {

118
freqtrade/rpc/api_server.py Normal file
View File

@ -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: <br>' \
'<a href=/daily?timescale=7>/daily?timescale=7</a>' \
'<br>' \
'<a href=/stop>/stop</a>' \
'<br>' \
'<a href=/start>/start</a>'
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:")

53
freqtrade/rpc/rest_client.py Executable file
View File

@ -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)

View File

@ -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 import logging
from typing import List from typing import List
@ -23,6 +23,13 @@ class RPCManager(object):
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade)) 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: def cleanup(self) -> None:
""" Stops all enabled rpc modules """ """ Stops all enabled rpc modules """
logger.info('Cleaning up rpc modules ...') logger.info('Cleaning up rpc modules ...')

View File

@ -23,3 +23,8 @@ coinmarketcap==5.0.3
# Required for plotting data # Required for plotting data
#plotly==2.3.0 #plotly==2.3.0
#Added for local rest client
Flask==1.0.2
flask-jsonpify==1.5.0
flask-restful==0.3.6