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
This commit is contained in:
parent
63c16b7f83
commit
95ba016558
@ -1,32 +1,26 @@
|
|||||||
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:
|
||||||
"""
|
"""
|
||||||
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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,21 +85,12 @@ 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" \
|
results.append(dict(
|
||||||
"*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,
|
trade_id=trade.id,
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
market_url=self._freqtrade.exchange.get_pair_detail_url(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,
|
open_rate=trade.open_rate,
|
||||||
close_rate=trade.close_rate,
|
close_rate=trade.close_rate,
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
@ -111,32 +100,8 @@ class RPC(object):
|
|||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['side'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
)
|
))
|
||||||
result.append(message)
|
return results
|
||||||
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
|
|
||||||
|
|
||||||
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 = []
|
||||||
|
Loading…
Reference in New Issue
Block a user