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,33 +1,27 @@
import json import json
import threading import threading
import logging 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 json import dumps
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from ipaddress import IPv4Address from ipaddress import IPv4Address
from freqtrade.rpc.api_server_common import MyApiApp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
""" app = Flask(__name__)
api server routes that do not need access to rpc.rpc
are held within api_server_common.api_server
"""
app = MyApiApp(__name__)
class ApiServer(RPC): 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 runs api server and provides rpc.rpc functionality to it
This class starts a none blocking thread the api server runs within 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
"""
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the api server, and init the super class RPC Init the api server, and init the super class RPC
@ -39,11 +33,20 @@ class ApiServer(RPC):
self._config = freqtrade.config self._config = freqtrade.config
# Register application handling # Register application handling
self.register_rest_other()
self.register_rest_rpc_urls() self.register_rest_rpc_urls()
thread = threading.Thread(target=self.run, daemon=True) thread = threading.Thread(target=self.run, daemon=True)
thread.start() 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): def register_rest_rpc_urls(self):
""" """
Registers flask app URLs that are calls to functonality in rpc.rpc. 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('/stop', 'stop', view_func=self.stop, methods=['GET'])
app.add_url_rule('/start', 'start', view_func=self.start, 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('/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): def run(self):
""" Method that runs flask app in its own thread forever """ """ Method that runs flask app in its own thread forever """
@ -82,17 +88,47 @@ class ApiServer(RPC):
def cleanup(self) -> None: def cleanup(self) -> None:
pass pass
def send_msg(self, msg: str) -> None: def send_msg(self, msg: Dict[str, str]) -> None:
pass pass
""" """
Define the application methods here, called by app.add_url_rule Define the application methods here, called by app.add_url_rule
each Telegram command should have a like local substitute each Telegram command should have a like local substitute
""" """
def stop_api(self):
""" For calling shutdown_api_server over via api server HTTP""" def page_not_found(self, error):
self.shutdown_api_server() """
return 'Api Server shutting down... ' 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): def daily(self):
""" """
@ -102,6 +138,7 @@ class ApiServer(RPC):
""" """
try: try:
timescale = request.args.get('timescale') timescale = request.args.get('timescale')
logger.info("LocalRPC - Daily Command Called")
timescale = int(timescale) timescale = int(timescale)
stats = self._rpc_daily_profit(timescale, stats = self._rpc_daily_profit(timescale,
@ -109,10 +146,45 @@ class ApiServer(RPC):
self._config['fiat_display_currency'] self._config['fiat_display_currency']
) )
stats = dumps(stats, indent=4, sort_keys=True, default=str) return json.dumps(stats, indent=4, sort_keys=True, default=str)
return stats
except RPCException as e: 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): def start(self):
""" """

View File

@ -5,14 +5,12 @@ import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from decimal import Decimal from decimal import Decimal
from typing import Dict, Any, List from typing import Dict, List, Any
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
from numpy import mean, nan_to_num from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
@ -51,20 +49,20 @@ class RPC(object):
""" """
self._freqtrade = freqtrade self._freqtrade = freqtrade
@property
def name(self) -> str:
""" Returns the lowercase name of the implementation """
return self.__class__.__name__.lower()
@abstractmethod @abstractmethod
def cleanup(self) -> None: def cleanup(self) -> None:
""" Cleanup pending module resources """ """ Cleanup pending module resources """
@property
@abstractmethod @abstractmethod
def name(self) -> str: def send_msg(self, msg: Dict[str, str]) -> None:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """ """ 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 Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
@ -76,7 +74,7 @@ class RPC(object):
elif not trades: elif not trades:
raise RPCException('no active trade') raise RPCException('no active trade')
else: else:
result = [] results = []
for trade in trades: for trade in trades:
order = None order = None
if trade.open_order_id: if trade.open_order_id:
@ -87,56 +85,23 @@ class RPC(object):
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) 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: results.append(dict(
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trade_id=trade.id,
if self._freqtrade.state != State.RUNNING: pair=trade.pair,
raise RPCException('trader is not running') market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
elif not trades: open_date=arrow.get(trade.open_date),
raise RPCException('no active order') open_rate=trade.open_rate,
else: close_rate=trade.close_rate,
trades_list = [] current_rate=current_rate,
for trade in trades: amount=round(trade.amount, 8),
# calculate profit and send message to user close_profit=fmt_close_profit,
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_profit=round(current_profit * 100, 2),
trades_list.append([ open_order='({} {} rem={:.8f})'.format(
trade.id, order['type'], order['side'], order['remaining']
trade.pair, ) if order else None,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), ))
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) return results
])
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( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
@ -190,6 +155,9 @@ class RPC(object):
""" Returns cumulative profit statistics """ """ Returns cumulative profit statistics """
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
if not trades:
raise RPCException('No trades found')
profit_all_coin = [] profit_all_coin = []
profit_all_percent = [] profit_all_percent = []
profit_closed_coin = [] profit_closed_coin = []