2019-04-04 05:08:24 +00:00
|
|
|
import logging
|
2019-04-05 04:39:33 +00:00
|
|
|
import threading
|
2020-07-31 05:31:20 +00:00
|
|
|
from copy import deepcopy
|
2019-05-18 08:34:30 +00:00
|
|
|
from datetime import date, datetime
|
2019-04-05 04:39:33 +00:00
|
|
|
from ipaddress import IPv4Address
|
2020-07-31 05:31:20 +00:00
|
|
|
from pathlib import Path
|
2020-05-10 08:35:38 +00:00
|
|
|
from typing import Any, Callable, Dict
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-07 11:09:53 +00:00
|
|
|
from arrow import Arrow
|
2019-04-05 04:39:33 +00:00
|
|
|
from flask import Flask, jsonify, request
|
2019-04-07 11:09:53 +00:00
|
|
|
from flask.json import JSONEncoder
|
2020-05-16 05:07:24 +00:00
|
|
|
from flask_cors import CORS
|
2020-09-28 17:39:41 +00:00
|
|
|
from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
|
|
|
|
get_jwt_identity, jwt_refresh_token_required,
|
2020-05-10 08:35:38 +00:00
|
|
|
verify_jwt_in_request_optional)
|
|
|
|
from werkzeug.security import safe_str_cmp
|
2019-05-18 07:50:19 +00:00
|
|
|
from werkzeug.serving import make_server
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-06 18:08:01 +00:00
|
|
|
from freqtrade.__init__ import __version__
|
2020-07-31 05:31:20 +00:00
|
|
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES
|
2020-09-11 04:44:20 +00:00
|
|
|
from freqtrade.persistence import Trade
|
2020-07-02 09:27:33 +00:00
|
|
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
2020-09-11 04:44:20 +00:00
|
|
|
from freqtrade.rpc.rpc import RPC, RPCException
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2019-04-04 05:08:24 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2019-04-07 11:09:53 +00:00
|
|
|
|
2019-05-15 05:12:33 +00:00
|
|
|
BASE_URI = "/api/v1"
|
|
|
|
|
2019-04-07 11:09:53 +00:00
|
|
|
|
2020-06-15 05:53:23 +00:00
|
|
|
class FTJSONEncoder(JSONEncoder):
|
2019-04-07 11:09:53 +00:00
|
|
|
def default(self, obj):
|
|
|
|
try:
|
|
|
|
if isinstance(obj, Arrow):
|
|
|
|
return obj.for_json()
|
2019-05-11 11:31:48 +00:00
|
|
|
elif isinstance(obj, datetime):
|
2020-06-09 06:07:34 +00:00
|
|
|
return obj.strftime(DATETIME_PRINT_FORMAT)
|
2020-06-15 05:53:23 +00:00
|
|
|
elif isinstance(obj, date):
|
|
|
|
return obj.strftime("%Y-%m-%d")
|
2019-04-07 11:09:53 +00:00
|
|
|
iterable = iter(obj)
|
|
|
|
except TypeError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
return list(iterable)
|
|
|
|
return JSONEncoder.default(self, obj)
|
|
|
|
|
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency
|
|
|
|
def require_login(func: Callable[[Any, Any], Any]):
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
def func_wrapper(obj, *args, **kwargs):
|
2020-05-10 08:35:38 +00:00
|
|
|
verify_jwt_in_request_optional()
|
2019-10-21 17:43:44 +00:00
|
|
|
auth = request.authorization
|
2020-05-10 17:43:16 +00:00
|
|
|
if get_jwt_identity() or auth and obj.check_auth(auth.username, auth.password):
|
2019-10-21 17:43:44 +00:00
|
|
|
return func(obj, *args, **kwargs)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "Unauthorized"}), 401
|
2019-04-09 04:59:07 +00:00
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
return func_wrapper
|
2019-04-09 04:59:07 +00:00
|
|
|
|
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
|
2020-08-04 14:01:41 +00:00
|
|
|
def rpc_catch_errors(func: Callable[..., Any]):
|
2019-04-09 04:59:07 +00:00
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
def func_wrapper(obj, *args, **kwargs):
|
|
|
|
|
|
|
|
try:
|
|
|
|
return func(obj, *args, **kwargs)
|
|
|
|
except RPCException as e:
|
|
|
|
logger.exception("API Error calling %s: %s", func.__name__, e)
|
|
|
|
return obj.rest_error(f"Error querying {func.__name__}: {e}")
|
|
|
|
|
|
|
|
return func_wrapper
|
2019-05-25 12:25:16 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
|
2020-09-11 04:44:20 +00:00
|
|
|
def shutdown_session(exception=None):
|
|
|
|
# Remove scoped session
|
|
|
|
Trade.session.remove()
|
|
|
|
|
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
class ApiServer(RPC):
|
|
|
|
"""
|
|
|
|
This class runs api server and provides rpc.rpc functionality to it
|
2019-05-25 12:11:30 +00:00
|
|
|
|
2019-10-21 21:03:11 +00:00
|
|
|
This class starts a non-blocking thread the api server runs within
|
2019-10-21 17:43:44 +00:00
|
|
|
"""
|
2019-05-25 12:11:30 +00:00
|
|
|
|
2019-10-21 17:43:44 +00:00
|
|
|
def check_auth(self, username, password):
|
2020-05-10 08:35:38 +00:00
|
|
|
return (safe_str_cmp(username, self._config['api_server'].get('username')) and
|
|
|
|
safe_str_cmp(password, self._config['api_server'].get('password')))
|
2019-05-25 12:11:30 +00:00
|
|
|
|
2019-04-04 05:08:24 +00:00
|
|
|
def __init__(self, freqtrade) -> None:
|
|
|
|
"""
|
|
|
|
Init the api server, and init the super class RPC
|
|
|
|
:param freqtrade: Instance of a freqtrade bot
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
super().__init__(freqtrade)
|
|
|
|
|
|
|
|
self._config = freqtrade.config
|
2019-05-10 05:07:14 +00:00
|
|
|
self.app = Flask(__name__)
|
2020-05-20 05:01:14 +00:00
|
|
|
self._cors = CORS(self.app,
|
2020-06-24 18:32:19 +00:00
|
|
|
resources={r"/api/*": {
|
|
|
|
"supports_credentials": True,
|
|
|
|
"origins": self._config['api_server'].get('CORS_origins', [])}}
|
2020-05-20 05:01:14 +00:00
|
|
|
)
|
2020-05-10 08:35:38 +00:00
|
|
|
|
|
|
|
# Setup the Flask-JWT-Extended extension
|
2020-05-10 17:42:06 +00:00
|
|
|
self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get(
|
|
|
|
'jwt_secret_key', 'super-secret')
|
2020-05-10 08:35:38 +00:00
|
|
|
|
|
|
|
self.jwt = JWTManager(self.app)
|
2020-06-15 05:53:23 +00:00
|
|
|
self.app.json_encoder = FTJSONEncoder
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2020-09-11 04:44:20 +00:00
|
|
|
self.app.teardown_appcontext(shutdown_session)
|
|
|
|
|
2019-04-04 05:08:24 +00:00
|
|
|
# Register application handling
|
|
|
|
self.register_rest_rpc_urls()
|
|
|
|
|
2020-07-02 09:27:33 +00:00
|
|
|
if self._config.get('fiat_display_currency', None):
|
|
|
|
self._fiat_converter = CryptoToFiatConverter()
|
|
|
|
|
2019-04-04 05:08:24 +00:00
|
|
|
thread = threading.Thread(target=self.run, daemon=True)
|
|
|
|
thread.start()
|
|
|
|
|
2019-04-04 17:34:19 +00:00
|
|
|
def cleanup(self) -> None:
|
2019-04-04 18:21:26 +00:00
|
|
|
logger.info("Stopping API Server")
|
2019-05-18 07:50:19 +00:00
|
|
|
self.srv.shutdown()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""
|
|
|
|
Method that runs flask app in its own thread forever.
|
|
|
|
Section to handle configuration and running of the Rest server
|
|
|
|
also to check and warn if not bound to a loopback, warn on security risk.
|
|
|
|
"""
|
|
|
|
rest_ip = self._config['api_server']['listen_ip_address']
|
|
|
|
rest_port = self._config['api_server']['listen_port']
|
|
|
|
|
|
|
|
logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}')
|
|
|
|
if not IPv4Address(rest_ip).is_loopback:
|
|
|
|
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
|
|
|
|
logger.warning("SECURITY WARNING - This is insecure please set to your loopback,"
|
|
|
|
"e.g 127.0.0.1 in config.json")
|
|
|
|
|
2019-05-26 12:40:03 +00:00
|
|
|
if not self._config['api_server'].get('password'):
|
|
|
|
logger.warning("SECURITY WARNING - No password for local REST Server defined. "
|
|
|
|
"Please make sure that this is intentional!")
|
|
|
|
|
2019-05-18 07:50:19 +00:00
|
|
|
# Run the Server
|
2019-05-18 11:36:51 +00:00
|
|
|
logger.info('Starting Local Rest Server.')
|
2019-05-18 07:50:19 +00:00
|
|
|
try:
|
|
|
|
self.srv = make_server(rest_ip, rest_port, self.app)
|
|
|
|
self.srv.serve_forever()
|
|
|
|
except Exception:
|
2019-05-18 11:36:51 +00:00
|
|
|
logger.exception("Api server failed to start.")
|
|
|
|
logger.info('Local Rest Server started.')
|
2019-04-04 17:34:19 +00:00
|
|
|
|
|
|
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
2019-04-07 11:09:53 +00:00
|
|
|
"""
|
|
|
|
We don't push to endpoints at the moment.
|
|
|
|
Take a look at webhooks for that functionality.
|
|
|
|
"""
|
2019-04-04 17:34:19 +00:00
|
|
|
pass
|
|
|
|
|
2019-04-04 18:21:26 +00:00
|
|
|
def rest_dump(self, return_value):
|
|
|
|
""" Helper function to jsonify object for a webserver """
|
|
|
|
return jsonify(return_value)
|
|
|
|
|
2019-04-07 11:09:53 +00:00
|
|
|
def rest_error(self, error_msg):
|
|
|
|
return jsonify({"error": error_msg}), 502
|
|
|
|
|
2019-04-04 05:08:24 +00:00
|
|
|
def register_rest_rpc_urls(self):
|
|
|
|
"""
|
|
|
|
Registers flask app URLs that are calls to functonality in rpc.rpc.
|
|
|
|
|
|
|
|
First two arguments passed are /URL and 'Label'
|
|
|
|
Label can be used as a shortcut when refactoring
|
|
|
|
:return:
|
|
|
|
"""
|
2019-05-18 11:36:51 +00:00
|
|
|
self.app.register_error_handler(404, self.page_not_found)
|
|
|
|
|
2019-04-07 11:22:44 +00:00
|
|
|
# Actions to control the bot
|
2020-05-10 08:35:38 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/token/login', 'login',
|
2020-05-10 17:37:41 +00:00
|
|
|
view_func=self._token_login, methods=['POST'])
|
2020-05-10 08:35:38 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/token/refresh', 'token_refresh',
|
2020-05-10 17:37:41 +00:00
|
|
|
view_func=self._token_refresh, methods=['POST'])
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/start', 'start',
|
|
|
|
view_func=self._start, methods=['POST'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
|
|
|
view_func=self._stopbuy, methods=['POST'])
|
2020-06-09 21:03:55 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config',
|
|
|
|
view_func=self._reload_config, methods=['POST'])
|
2019-04-07 11:22:44 +00:00
|
|
|
# Info commands
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
|
|
|
view_func=self._balance, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
|
2020-08-14 13:44:36 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
|
|
|
|
view_func=self._profit, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
|
|
|
|
view_func=self._performance, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
|
|
|
|
view_func=self._status, methods=['GET'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/version', 'version',
|
|
|
|
view_func=self._version, methods=['GET'])
|
2019-11-17 13:56:08 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config',
|
|
|
|
view_func=self._show_config, methods=['GET'])
|
2019-11-11 19:09:58 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/ping', 'ping',
|
|
|
|
view_func=self._ping, methods=['GET'])
|
2020-04-05 14:14:02 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
|
|
|
|
view_func=self._trades, methods=['GET'])
|
2020-08-04 12:41:22 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
|
|
|
|
view_func=self._trades_delete, methods=['DELETE'])
|
2020-09-11 18:15:54 +00:00
|
|
|
|
2020-07-02 05:10:56 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles',
|
|
|
|
view_func=self._analysed_candles, methods=['GET'])
|
2020-06-12 17:32:44 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history',
|
|
|
|
view_func=self._analysed_history, methods=['GET'])
|
2020-06-23 04:49:53 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config',
|
|
|
|
view_func=self._plot_config, methods=['GET'])
|
2020-07-31 05:31:20 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies',
|
|
|
|
view_func=self._list_strategies, methods=['GET'])
|
2020-08-01 15:49:59 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs',
|
|
|
|
view_func=self._list_available_pairs, methods=['GET'])
|
2020-07-31 05:31:20 +00:00
|
|
|
|
2019-04-26 07:59:08 +00:00
|
|
|
# Combined actions and infos
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
|
2019-05-10 05:07:14 +00:00
|
|
|
methods=['GET', 'POST'])
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist,
|
2019-05-10 05:07:14 +00:00
|
|
|
methods=['GET'])
|
2019-05-15 05:12:33 +00:00
|
|
|
self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy',
|
|
|
|
view_func=self._forcebuy, methods=['POST'])
|
|
|
|
self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell,
|
2019-05-10 05:07:14 +00:00
|
|
|
methods=['POST'])
|
2019-04-26 10:50:13 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-04-04 05:08:24 +00:00
|
|
|
def page_not_found(self, error):
|
|
|
|
"""
|
|
|
|
Return "404 not found", 404.
|
|
|
|
"""
|
2019-04-04 18:21:26 +00:00
|
|
|
return self.rest_dump({
|
2019-04-04 05:08:24 +00:00
|
|
|
'status': 'error',
|
2019-05-11 11:18:11 +00:00
|
|
|
'reason': f"There's no API call for {request.base_url}.",
|
2019-04-04 05:08:24 +00:00
|
|
|
'code': 404
|
|
|
|
}), 404
|
|
|
|
|
2020-05-10 08:35:38 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
2020-05-10 17:37:41 +00:00
|
|
|
def _token_login(self):
|
2020-05-10 08:35:38 +00:00
|
|
|
"""
|
|
|
|
Handler for /token/login
|
|
|
|
Returns a JWT token
|
|
|
|
"""
|
|
|
|
auth = request.authorization
|
|
|
|
if auth and self.check_auth(auth.username, auth.password):
|
|
|
|
keystuff = {'u': auth.username}
|
|
|
|
ret = {
|
|
|
|
'access_token': create_access_token(identity=keystuff),
|
|
|
|
'refresh_token': create_refresh_token(identity=keystuff),
|
|
|
|
}
|
|
|
|
return self.rest_dump(ret)
|
|
|
|
|
|
|
|
return jsonify({"error": "Unauthorized"}), 401
|
|
|
|
|
|
|
|
@jwt_refresh_token_required
|
|
|
|
@rpc_catch_errors
|
2020-05-10 17:37:41 +00:00
|
|
|
def _token_refresh(self):
|
2020-05-10 08:35:38 +00:00
|
|
|
"""
|
|
|
|
Handler for /token/refresh
|
|
|
|
Returns a JWT token based on a JWT refresh token
|
|
|
|
"""
|
|
|
|
current_user = get_jwt_identity()
|
|
|
|
new_token = create_access_token(identity=current_user, fresh=False)
|
|
|
|
|
|
|
|
ret = {'access_token': new_token}
|
|
|
|
return self.rest_dump(ret)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _start(self):
|
|
|
|
"""
|
|
|
|
Handler for /start.
|
|
|
|
Starts TradeThread in bot if stopped.
|
|
|
|
"""
|
|
|
|
msg = self._rpc_start()
|
|
|
|
return self.rest_dump(msg)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _stop(self):
|
|
|
|
"""
|
|
|
|
Handler for /stop.
|
|
|
|
Stops TradeThread in bot if running
|
|
|
|
"""
|
|
|
|
msg = self._rpc_stop()
|
|
|
|
return self.rest_dump(msg)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _stopbuy(self):
|
|
|
|
"""
|
|
|
|
Handler for /stopbuy.
|
|
|
|
Sets max_open_trades to 0 and gracefully sells all open trades
|
|
|
|
"""
|
|
|
|
msg = self._rpc_stopbuy()
|
|
|
|
return self.rest_dump(msg)
|
|
|
|
|
2019-11-11 19:09:58 +00:00
|
|
|
@rpc_catch_errors
|
|
|
|
def _ping(self):
|
|
|
|
"""
|
|
|
|
simple poing version
|
|
|
|
"""
|
|
|
|
return self.rest_dump({"status": "pong"})
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _version(self):
|
|
|
|
"""
|
|
|
|
Prints the bot's version
|
|
|
|
"""
|
|
|
|
return self.rest_dump({"version": __version__})
|
|
|
|
|
2019-11-17 13:56:08 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _show_config(self):
|
|
|
|
"""
|
|
|
|
Prints the bot's version
|
|
|
|
"""
|
2020-08-01 15:55:03 +00:00
|
|
|
return self.rest_dump(self._rpc_show_config(self._config))
|
2019-11-17 13:56:08 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2020-06-09 21:03:55 +00:00
|
|
|
def _reload_config(self):
|
2019-04-06 18:08:01 +00:00
|
|
|
"""
|
2020-06-09 21:03:55 +00:00
|
|
|
Handler for /reload_config.
|
2019-04-06 18:08:01 +00:00
|
|
|
Triggers a config file reload
|
|
|
|
"""
|
2020-06-09 21:03:55 +00:00
|
|
|
msg = self._rpc_reload_config()
|
2019-04-06 18:08:01 +00:00
|
|
|
return self.rest_dump(msg)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _count(self):
|
|
|
|
"""
|
|
|
|
Handler for /count.
|
|
|
|
Returns the number of trades running
|
|
|
|
"""
|
2019-04-09 04:59:07 +00:00
|
|
|
msg = self._rpc_count()
|
|
|
|
return self.rest_dump(msg)
|
2019-04-06 18:08:01 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _daily(self):
|
2019-04-04 05:08:24 +00:00
|
|
|
"""
|
|
|
|
Returns the last X days trading stats summary.
|
|
|
|
|
|
|
|
:return: stats
|
|
|
|
"""
|
2019-04-09 05:08:46 +00:00
|
|
|
timescale = request.args.get('timescale', 7)
|
2019-04-09 04:59:07 +00:00
|
|
|
timescale = int(timescale)
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-09 04:59:07 +00:00
|
|
|
stats = self._rpc_daily_profit(timescale,
|
|
|
|
self._config['stake_currency'],
|
2019-11-12 12:54:26 +00:00
|
|
|
self._config.get('fiat_display_currency', '')
|
2019-04-09 04:59:07 +00:00
|
|
|
)
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-09 04:59:07 +00:00
|
|
|
return self.rest_dump(stats)
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2020-08-14 13:44:36 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _get_logs(self):
|
|
|
|
"""
|
|
|
|
Returns latest logs
|
|
|
|
get:
|
|
|
|
param:
|
|
|
|
limit: Only get a certain number of records
|
|
|
|
"""
|
|
|
|
limit = int(request.args.get('limit', 0)) or None
|
|
|
|
return self.rest_dump(self._rpc_get_logs(limit))
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-26 08:06:46 +00:00
|
|
|
def _edge(self):
|
|
|
|
"""
|
|
|
|
Returns information related to Edge.
|
|
|
|
:return: edge stats
|
|
|
|
"""
|
|
|
|
stats = self._rpc_edge()
|
|
|
|
|
|
|
|
return self.rest_dump(stats)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-06 18:08:01 +00:00
|
|
|
def _profit(self):
|
2019-04-04 05:08:24 +00:00
|
|
|
"""
|
|
|
|
Handler for /profit.
|
|
|
|
|
|
|
|
Returns a cumulative profit statistics
|
|
|
|
:return: stats
|
|
|
|
"""
|
|
|
|
|
2019-04-09 04:59:07 +00:00
|
|
|
stats = self._rpc_trade_statistics(self._config['stake_currency'],
|
2019-11-25 06:12:30 +00:00
|
|
|
self._config.get('fiat_display_currency')
|
2019-04-09 04:59:07 +00:00
|
|
|
)
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-09 04:59:07 +00:00
|
|
|
return self.rest_dump(stats)
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-26 08:03:54 +00:00
|
|
|
def _performance(self):
|
|
|
|
"""
|
|
|
|
Handler for /performance.
|
|
|
|
|
|
|
|
Returns a cumulative performance statistics
|
|
|
|
:return: stats
|
|
|
|
"""
|
|
|
|
stats = self._rpc_performance()
|
|
|
|
|
|
|
|
return self.rest_dump(stats)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-07 11:09:53 +00:00
|
|
|
def _status(self):
|
2019-04-04 05:08:24 +00:00
|
|
|
"""
|
2019-04-19 04:56:01 +00:00
|
|
|
Handler for /status.
|
2019-04-04 05:08:24 +00:00
|
|
|
|
2019-04-07 11:09:53 +00:00
|
|
|
Returns the current status of the trades in json format
|
2019-04-04 05:08:24 +00:00
|
|
|
"""
|
2019-11-17 13:40:59 +00:00
|
|
|
try:
|
|
|
|
results = self._rpc_trade_status()
|
|
|
|
return self.rest_dump(results)
|
|
|
|
except RPCException:
|
|
|
|
return self.rest_dump([])
|
2019-04-07 11:22:44 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-07 11:22:44 +00:00
|
|
|
def _balance(self):
|
|
|
|
"""
|
2019-04-19 04:56:01 +00:00
|
|
|
Handler for /balance.
|
2019-04-07 11:22:44 +00:00
|
|
|
|
|
|
|
Returns the current status of the trades in json format
|
|
|
|
"""
|
2019-11-15 05:33:07 +00:00
|
|
|
results = self._rpc_balance(self._config['stake_currency'],
|
|
|
|
self._config.get('fiat_display_currency', ''))
|
2019-04-09 04:59:07 +00:00
|
|
|
return self.rest_dump(results)
|
2020-04-05 14:47:46 +00:00
|
|
|
|
2020-04-05 14:14:02 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _trades(self):
|
|
|
|
"""
|
|
|
|
Handler for /trades.
|
|
|
|
|
|
|
|
Returns the X last trades in json format
|
|
|
|
"""
|
2020-04-06 09:00:31 +00:00
|
|
|
limit = int(request.args.get('limit', 0))
|
|
|
|
results = self._rpc_trade_history(limit)
|
2020-04-05 14:14:02 +00:00
|
|
|
return self.rest_dump(results)
|
2019-04-09 04:59:07 +00:00
|
|
|
|
2020-08-04 12:41:22 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _trades_delete(self, tradeid):
|
|
|
|
"""
|
|
|
|
Handler for DELETE /trades/<tradeid> endpoint.
|
2020-08-04 17:43:05 +00:00
|
|
|
Removes the trade from the database (tries to cancel open orders first!)
|
|
|
|
get:
|
|
|
|
param:
|
|
|
|
tradeid: Numeric trade-id assigned to the trade.
|
2020-08-04 12:41:22 +00:00
|
|
|
"""
|
|
|
|
result = self._rpc_delete(tradeid)
|
|
|
|
return self.rest_dump(result)
|
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-09 04:59:07 +00:00
|
|
|
def _whitelist(self):
|
|
|
|
"""
|
2019-04-19 04:56:01 +00:00
|
|
|
Handler for /whitelist.
|
2019-04-09 04:59:07 +00:00
|
|
|
"""
|
|
|
|
results = self._rpc_whitelist()
|
|
|
|
return self.rest_dump(results)
|
2019-04-19 04:56:01 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-19 04:56:01 +00:00
|
|
|
def _blacklist(self):
|
|
|
|
"""
|
|
|
|
Handler for /blacklist.
|
|
|
|
"""
|
2019-04-26 07:55:36 +00:00
|
|
|
add = request.json.get("blacklist", None) if request.method == 'POST' else None
|
|
|
|
results = self._rpc_blacklist(add)
|
2019-04-19 04:56:01 +00:00
|
|
|
return self.rest_dump(results)
|
2019-04-26 10:50:13 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-26 10:50:13 +00:00
|
|
|
def _forcebuy(self):
|
|
|
|
"""
|
|
|
|
Handler for /forcebuy.
|
|
|
|
"""
|
|
|
|
asset = request.json.get("pair")
|
|
|
|
price = request.json.get("price", None)
|
|
|
|
trade = self._rpc_forcebuy(asset, price)
|
2019-05-15 04:51:23 +00:00
|
|
|
if trade:
|
|
|
|
return self.rest_dump(trade.to_json())
|
|
|
|
else:
|
|
|
|
return self.rest_dump({"status": f"Error buying pair {asset}."})
|
2019-04-26 10:50:13 +00:00
|
|
|
|
2019-05-25 12:11:30 +00:00
|
|
|
@require_login
|
2019-05-18 11:39:12 +00:00
|
|
|
@rpc_catch_errors
|
2019-04-26 10:50:13 +00:00
|
|
|
def _forcesell(self):
|
|
|
|
"""
|
|
|
|
Handler for /forcesell.
|
|
|
|
"""
|
|
|
|
tradeid = request.json.get("tradeid")
|
|
|
|
results = self._rpc_forcesell(tradeid)
|
|
|
|
return self.rest_dump(results)
|
2020-06-12 17:32:44 +00:00
|
|
|
|
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
2020-07-02 05:10:56 +00:00
|
|
|
def _analysed_candles(self):
|
2020-06-12 17:32:44 +00:00
|
|
|
"""
|
2020-09-11 18:15:54 +00:00
|
|
|
Handler for /pair_candles.
|
2020-07-02 05:10:56 +00:00
|
|
|
Returns the dataframe the bot is using during live/dry operations.
|
2020-06-15 05:53:23 +00:00
|
|
|
Takes the following get arguments:
|
|
|
|
get:
|
|
|
|
parameters:
|
|
|
|
- pair: Pair
|
|
|
|
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
|
|
|
|
- limit: Limit return length to the latest X candles
|
2020-06-12 17:32:44 +00:00
|
|
|
"""
|
|
|
|
pair = request.args.get("pair")
|
|
|
|
timeframe = request.args.get("timeframe")
|
2020-06-15 06:47:18 +00:00
|
|
|
limit = request.args.get("limit", type=int)
|
2020-06-12 17:32:44 +00:00
|
|
|
|
2020-07-02 05:10:56 +00:00
|
|
|
results = self._analysed_dataframe(pair, timeframe, limit)
|
2020-06-12 17:32:44 +00:00
|
|
|
return self.rest_dump(results)
|
2020-06-23 04:49:53 +00:00
|
|
|
|
2020-07-02 05:10:56 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _analysed_history(self):
|
|
|
|
"""
|
|
|
|
Handler for /pair_history.
|
|
|
|
Returns the dataframe of a given timerange
|
|
|
|
Takes the following get arguments:
|
|
|
|
get:
|
2020-08-01 15:49:59 +00:00
|
|
|
parameters:
|
2020-07-02 05:10:56 +00:00
|
|
|
- pair: Pair
|
|
|
|
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
|
2020-07-26 18:31:11 +00:00
|
|
|
- strategy: Strategy to use - Must exist in configured strategy-path!
|
2020-07-02 05:10:56 +00:00
|
|
|
- timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD))
|
|
|
|
are als possible. If omitted uses all available data.
|
|
|
|
"""
|
|
|
|
pair = request.args.get("pair")
|
|
|
|
timeframe = request.args.get("timeframe")
|
|
|
|
timerange = request.args.get("timerange")
|
2020-07-26 18:31:11 +00:00
|
|
|
strategy = request.args.get("strategy")
|
2020-08-01 15:12:32 +00:00
|
|
|
|
|
|
|
if not pair or not timeframe or not timerange or not strategy:
|
|
|
|
return self.rest_error("Mandatory parameter missing.")
|
|
|
|
|
2020-07-26 18:31:11 +00:00
|
|
|
config = deepcopy(self._config)
|
|
|
|
config.update({
|
|
|
|
'strategy': strategy,
|
|
|
|
})
|
|
|
|
results = self._rpc_analysed_history_full(config, pair, timeframe, timerange)
|
2020-07-02 05:10:56 +00:00
|
|
|
return self.rest_dump(results)
|
|
|
|
|
2020-06-23 04:49:53 +00:00
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _plot_config(self):
|
|
|
|
"""
|
|
|
|
Handler for /plot_config.
|
|
|
|
"""
|
|
|
|
return self.rest_dump(self._rpc_plot_config())
|
2020-07-31 05:31:20 +00:00
|
|
|
|
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _list_strategies(self):
|
|
|
|
directory = Path(self._config.get(
|
|
|
|
'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES))
|
|
|
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
|
|
|
strategy_objs = StrategyResolver.search_all_objects(directory, False)
|
|
|
|
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
|
|
|
|
2020-08-01 15:12:32 +00:00
|
|
|
return self.rest_dump({'strategies': [x['name'] for x in strategy_objs]})
|
2020-08-01 15:49:59 +00:00
|
|
|
|
|
|
|
@require_login
|
|
|
|
@rpc_catch_errors
|
|
|
|
def _list_available_pairs(self):
|
|
|
|
"""
|
|
|
|
Handler for /available_pairs.
|
|
|
|
Returns an object, with pairs, available pair length and pair_interval combinations
|
|
|
|
Takes the following get arguments:
|
|
|
|
get:
|
|
|
|
parameters:
|
|
|
|
- stake_currency: Filter on this stake currency
|
|
|
|
- timeframe: Timeframe to get data for Filter elements to this timeframe
|
|
|
|
"""
|
|
|
|
timeframe = request.args.get("timeframe")
|
|
|
|
stake_currency = request.args.get("stake_currency")
|
|
|
|
|
|
|
|
from freqtrade.data.history import get_datahandler
|
|
|
|
dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None))
|
|
|
|
|
|
|
|
pair_interval = dh.ohlcv_get_available_data(self._config['datadir'])
|
|
|
|
|
|
|
|
if timeframe:
|
|
|
|
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
|
|
|
|
if stake_currency:
|
|
|
|
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
|
|
|
|
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
|
|
|
|
|
|
|
pairs = list({x[0] for x in pair_interval})
|
|
|
|
|
|
|
|
result = {
|
|
|
|
'length': len(pairs),
|
|
|
|
'pairs': pairs,
|
|
|
|
'pair_interval': pair_interval,
|
|
|
|
}
|
|
|
|
return self.rest_dump(result)
|