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 = []