From 95ba016558f8a0167df5240c5959885933905079 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 25 Jun 2018 19:55:28 +0000 Subject: [PATCH] rpc.py Added RPCExcpetion if trades is empty on trade.query api_server Return e on exception created server 500 error. Returned text for client, and sent e to logger Added profit and status table functions. Will look to add unit tests for these two --- freqtrade/rpc/api_server.py | 116 +++++++++++++++++++++++++++++------- freqtrade/rpc/rpc.py | 88 +++++++++------------------ 2 files changed, 122 insertions(+), 82 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e1897ff3d..e117c1362 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,33 +1,27 @@ import json import threading import logging +# import json +from typing import Dict -from flask import request +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 -from freqtrade.rpc.api_server_common import MyApiApp logger = logging.getLogger(__name__) -""" -api server routes that do not need access to rpc.rpc -are held within api_server_common.api_server -""" -app = MyApiApp(__name__) +app = Flask(__name__) class ApiServer(RPC): """ + This class is for REST calls across api server This class runs api server and provides rpc.rpc functionality to it - This class starts a none blocking thread the api server runs within - Any routes that require access to rpc.rpc defs are held within this - class. - - Any routes that do not require access to rpc.rcp should be registered - in api_server_common.MyApiApp - """ + This class starts a none blocking thread the api server runs within\ + """ def __init__(self, freqtrade) -> None: """ Init the api server, and init the super class RPC @@ -39,11 +33,20 @@ class ApiServer(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 register_rest_other(self): + """ + Registers flask app URLs that are not calls to functionality in rpc.rpc. + :return: + """ + app.register_error_handler(404, self.page_not_found) + app.add_url_rule('/', 'hello', view_func=self.hello, methods=['GET']) + def register_rest_rpc_urls(self): """ Registers flask app URLs that are calls to functonality in rpc.rpc. @@ -55,6 +58,9 @@ class ApiServer(RPC): app.add_url_rule('/stop', 'stop', view_func=self.stop, methods=['GET']) app.add_url_rule('/start', 'start', view_func=self.start, methods=['GET']) app.add_url_rule('/daily', 'daily', view_func=self.daily, methods=['GET']) + app.add_url_rule('/profit', 'profit', view_func=self.profit, methods=['GET']) + app.add_url_rule('/status_table', 'status_table', + view_func=self.status_table, methods=['GET']) def run(self): """ Method that runs flask app in its own thread forever """ @@ -82,17 +88,47 @@ 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 """ Define the application methods here, called by app.add_url_rule each Telegram command should have a like local substitute """ - def stop_api(self): - """ For calling shutdown_api_server over via api server HTTP""" - self.shutdown_api_server() - return 'Api Server shutting down... ' + + def page_not_found(self, error): + """ + Return "404 not found", 404. + """ + return json.dumps({ + 'status': 'error', + 'reason': '''There's no API call for %s''' % request.base_url, + 'code': 404 + }), 404 + + def hello(self): + """ + None critical but helpful default index page. + + That lists URLs added to the flask server. + This may be deprecated at any time. + :return: index.html + """ + rest_cmds = 'Commands implemented:
' \ + 'Show 7 days of stats' \ + '
' \ + 'Stop the Trade thread' \ + '
' \ + 'Start the Traded thread' \ + '
' \ + 'Show profit summary' \ + '
' \ + 'Show status table - Open trades' \ + '
' \ + ' 404 page does not exist' \ + '
' + + return rest_cmds def daily(self): """ @@ -102,6 +138,7 @@ class ApiServer(RPC): """ try: timescale = request.args.get('timescale') + logger.info("LocalRPC - Daily Command Called") timescale = int(timescale) stats = self._rpc_daily_profit(timescale, @@ -109,10 +146,45 @@ class ApiServer(RPC): self._config['fiat_display_currency'] ) - stats = dumps(stats, indent=4, sort_keys=True, default=str) - return stats + return json.dumps(stats, indent=4, sort_keys=True, default=str) except RPCException as e: - return e + logger.exception("API Error querying daily:", e) + return "Error querying daily" + + def profit(self): + """ + Handler for /profit. + + Returns a cumulative profit statistics + :return: stats + """ + try: + logger.info("LocalRPC - Profit Command Called") + + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return json.dumps(stats, indent=4, sort_keys=True, default=str) + except RPCException as e: + logger.exception("API Error calling profit", e) + return "Error querying closed trades - maybe there are none" + + def status_table(self): + """ + Handler for /status table. + + Returns the current TradeThread status in table format + :return: results + """ + try: + results = self._rpc_trade_status() + return json.dumps(results, indent=4, sort_keys=True, default=str) + + except RPCException as e: + logger.exception("API Error calling status table", e) + return "Error querying open trades - maybe there are none." + def start(self): """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9bf92aa18..ce3d62ed7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -5,14 +5,12 @@ import logging from abc import abstractmethod from datetime import datetime, timedelta, date from decimal import Decimal -from typing import Dict, Any, List +from typing import Dict, List, Any import arrow import sqlalchemy as sql from numpy import mean, nan_to_num -from pandas import DataFrame -from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.state import State @@ -51,20 +49,20 @@ 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: + def send_msg(self, msg: Dict[str, 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 @@ -76,7 +74,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: @@ -87,56 +85,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=self._freqtrade.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 - 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 = self._freqtrade.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 + results.append(dict( + trade_id=trade.id, + pair=trade.pair, + market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), + open_date=arrow.get(trade.open_date), + 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_daily_profit( self, timescale: int, @@ -190,6 +155,9 @@ class RPC(object): """ Returns cumulative profit statistics """ trades = Trade.query.order_by(Trade.id).all() + if not trades: + raise RPCException('No trades found') + profit_all_coin = [] profit_all_percent = [] profit_closed_coin = []