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
This commit is contained in:
creslinux 2018-06-25 19:55:28 +00:00
parent 63c16b7f83
commit 95ba016558
2 changed files with 122 additions and 82 deletions

View File

@ -1,32 +1,26 @@
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:
"""
@ -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: <br>' \
'<a href=/daily?timescale=7>Show 7 days of stats</a>' \
'<br>' \
'<a href=/stop>Stop the Trade thread</a>' \
'<br>' \
'<a href=/start>Start the Traded thread</a>' \
'<br>' \
'<a href=/profit>Show profit summary</a>' \
'<br>' \
'<a href=/status_table>Show status table - Open trades</a>' \
'<br>' \
'<a href=/paypal> 404 page does not exist</a>' \
'<br>'
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):
"""

View File

@ -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,21 +85,12 @@ 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(
results.append(dict(
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_date=arrow.get(trade.open_date),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
@ -111,32 +100,8 @@ class RPC(object):
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
))
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 = []