2022-08-29 19:41:15 +00:00
|
|
|
import asyncio
|
2020-12-26 16:00:30 +00:00
|
|
|
import logging
|
2020-12-27 09:56:19 +00:00
|
|
|
from ipaddress import IPv4Address
|
2022-08-29 19:41:15 +00:00
|
|
|
from threading import Thread
|
2020-12-27 09:59:17 +00:00
|
|
|
from typing import Any, Dict
|
2020-12-25 19:07:12 +00:00
|
|
|
|
2022-04-27 11:13:11 +00:00
|
|
|
import orjson
|
2020-12-24 05:55:19 +00:00
|
|
|
import uvicorn
|
2020-12-25 14:50:19 +00:00
|
|
|
from fastapi import Depends, FastAPI
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2022-08-29 19:41:15 +00:00
|
|
|
# Look into alternatives
|
|
|
|
from janus import Queue as ThreadedQueue
|
2020-12-26 19:05:27 +00:00
|
|
|
from starlette.responses import JSONResponse
|
2020-12-24 05:55:19 +00:00
|
|
|
|
2020-12-31 19:02:27 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2021-01-01 18:13:32 +00:00
|
|
|
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
2022-08-29 19:41:15 +00:00
|
|
|
from freqtrade.rpc.api_server.ws.channel import ChannelManager
|
2020-12-26 16:00:30 +00:00
|
|
|
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
2020-12-24 05:55:19 +00:00
|
|
|
|
|
|
|
|
2020-12-26 16:00:30 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-02-22 11:11:27 +00:00
|
|
|
class FTJSONResponse(JSONResponse):
|
|
|
|
media_type = "application/json"
|
|
|
|
|
|
|
|
def render(self, content: Any) -> bytes:
|
|
|
|
"""
|
|
|
|
Use rapidjson for responses
|
|
|
|
Handles NaN and Inf / -Inf in a javascript way by default.
|
|
|
|
"""
|
2022-04-27 11:13:11 +00:00
|
|
|
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
|
2021-02-22 11:11:27 +00:00
|
|
|
|
|
|
|
|
2020-12-24 05:55:19 +00:00
|
|
|
class ApiServer(RPCHandler):
|
|
|
|
|
2020-12-31 19:02:27 +00:00
|
|
|
__instance = None
|
|
|
|
__initialized = False
|
|
|
|
|
2020-12-31 10:01:50 +00:00
|
|
|
_rpc: RPC
|
2021-01-02 14:13:32 +00:00
|
|
|
# Backtesting type: Backtesting
|
2021-01-06 14:05:54 +00:00
|
|
|
_bt = None
|
2021-07-10 09:19:23 +00:00
|
|
|
_bt_data = None
|
2021-01-06 14:05:54 +00:00
|
|
|
_bt_timerange = None
|
2021-07-10 09:19:23 +00:00
|
|
|
_bt_last_config: Dict[str, Any] = {}
|
2021-01-02 12:12:49 +00:00
|
|
|
_has_rpc: bool = False
|
2021-01-02 13:18:15 +00:00
|
|
|
_bgtask_running: bool = False
|
2020-12-25 14:50:19 +00:00
|
|
|
_config: Dict[str, Any] = {}
|
2022-01-22 06:11:59 +00:00
|
|
|
# Exchange - only available in webserver mode.
|
|
|
|
_exchange = None
|
2022-08-29 19:41:15 +00:00
|
|
|
# websocket message queue stuff
|
|
|
|
_channel_manager = None
|
|
|
|
_thread = None
|
|
|
|
_loop = None
|
2020-12-25 12:08:25 +00:00
|
|
|
|
2020-12-31 19:02:27 +00:00
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
This class is a singleton.
|
|
|
|
We'll only have one instance of it around.
|
|
|
|
"""
|
|
|
|
if ApiServer.__instance is None:
|
|
|
|
ApiServer.__instance = object.__new__(cls)
|
|
|
|
ApiServer.__initialized = False
|
|
|
|
return ApiServer.__instance
|
|
|
|
|
2022-08-30 17:04:16 +00:00
|
|
|
def __init__(self, config: Dict[str, Any], standalone: bool = False) -> None:
|
2021-01-02 13:18:15 +00:00
|
|
|
ApiServer._config = config
|
2020-12-31 19:02:27 +00:00
|
|
|
if self.__initialized and (standalone or self._standalone):
|
|
|
|
return
|
|
|
|
self._standalone: bool = standalone
|
2020-12-24 05:55:19 +00:00
|
|
|
self._server = None
|
2022-08-29 19:41:15 +00:00
|
|
|
self._queue = None
|
|
|
|
self._background_task = None
|
|
|
|
|
2021-01-02 13:18:15 +00:00
|
|
|
ApiServer.__initialized = True
|
2020-12-24 05:55:19 +00:00
|
|
|
|
2020-12-27 14:54:05 +00:00
|
|
|
api_config = self._config['api_server']
|
2020-12-25 12:08:25 +00:00
|
|
|
|
2022-08-29 19:41:15 +00:00
|
|
|
ApiServer._channel_manager = ChannelManager()
|
|
|
|
|
2020-12-27 14:54:05 +00:00
|
|
|
self.app = FastAPI(title="Freqtrade API",
|
2021-01-12 18:27:49 +00:00
|
|
|
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
|
2020-12-27 14:54:05 +00:00
|
|
|
redoc_url=None,
|
2021-02-22 11:11:27 +00:00
|
|
|
default_response_class=FTJSONResponse,
|
2020-12-27 14:54:05 +00:00
|
|
|
)
|
2020-12-24 05:55:19 +00:00
|
|
|
self.configure_app(self.app, self._config)
|
2022-08-27 05:40:13 +00:00
|
|
|
self.start_api()
|
2020-12-24 05:55:19 +00:00
|
|
|
|
2020-12-31 19:02:27 +00:00
|
|
|
def add_rpc_handler(self, rpc: RPC):
|
|
|
|
"""
|
|
|
|
Attach rpc handler
|
|
|
|
"""
|
2021-01-02 13:18:15 +00:00
|
|
|
if not self._has_rpc:
|
2020-12-31 19:02:27 +00:00
|
|
|
ApiServer._rpc = rpc
|
|
|
|
ApiServer._has_rpc = True
|
|
|
|
else:
|
|
|
|
# This should not happen assuming we didn't mess up.
|
|
|
|
raise OperationalException('RPC Handler already attached.')
|
|
|
|
|
2020-12-24 05:55:19 +00:00
|
|
|
def cleanup(self) -> None:
|
|
|
|
""" Cleanup pending module resources """
|
2021-06-20 10:54:05 +00:00
|
|
|
ApiServer._has_rpc = False
|
|
|
|
del ApiServer._rpc
|
|
|
|
if self._server and not self._standalone:
|
2020-12-27 09:56:19 +00:00
|
|
|
logger.info("Stopping API Server")
|
2020-12-24 05:55:19 +00:00
|
|
|
self._server.cleanup()
|
|
|
|
|
2022-08-29 19:41:15 +00:00
|
|
|
if self._thread and self._loop:
|
|
|
|
logger.info("Stopping API Server background tasks")
|
|
|
|
|
|
|
|
if self._background_task:
|
|
|
|
# Cancel the queue task
|
|
|
|
self._background_task.cancel()
|
|
|
|
|
|
|
|
self._thread.join()
|
|
|
|
|
2022-09-06 01:29:07 +00:00
|
|
|
self._thread = None
|
|
|
|
self._loop = None
|
|
|
|
self._background_task = None
|
|
|
|
|
2021-01-02 15:12:10 +00:00
|
|
|
@classmethod
|
|
|
|
def shutdown(cls):
|
|
|
|
cls.__initialized = False
|
|
|
|
del cls.__instance
|
|
|
|
cls.__instance = None
|
|
|
|
cls._has_rpc = False
|
|
|
|
cls._rpc = None
|
|
|
|
|
2020-12-24 05:55:19 +00:00
|
|
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
2022-08-29 19:41:15 +00:00
|
|
|
if self._queue:
|
|
|
|
sync_q = self._queue.sync_q
|
|
|
|
sync_q.put(msg)
|
2020-12-24 05:55:19 +00:00
|
|
|
|
2020-12-26 16:00:30 +00:00
|
|
|
def handle_rpc_exception(self, request, exc):
|
|
|
|
logger.exception(f"API Error calling: {exc}")
|
|
|
|
return JSONResponse(
|
|
|
|
status_code=502,
|
|
|
|
content={'error': f"Error querying {request.url.path}: {exc.message}"}
|
|
|
|
)
|
|
|
|
|
2020-12-25 12:08:25 +00:00
|
|
|
def configure_app(self, app: FastAPI, config):
|
2022-08-29 19:41:15 +00:00
|
|
|
from freqtrade.rpc.api_server.api_auth import (get_ws_token, http_basic_or_jwt_token,
|
|
|
|
router_login)
|
2021-04-02 18:00:14 +00:00
|
|
|
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
|
2021-01-01 18:13:32 +00:00
|
|
|
from freqtrade.rpc.api_server.api_v1 import router as api_v1
|
|
|
|
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
2022-08-29 19:41:15 +00:00
|
|
|
from freqtrade.rpc.api_server.api_ws import router as ws_router
|
2021-01-10 09:31:05 +00:00
|
|
|
from freqtrade.rpc.api_server.web_ui import router_ui
|
|
|
|
|
2020-12-25 12:08:25 +00:00
|
|
|
app.include_router(api_v1_public, prefix="/api/v1")
|
|
|
|
|
2020-12-25 14:50:19 +00:00
|
|
|
app.include_router(api_v1, prefix="/api/v1",
|
2020-12-26 07:48:15 +00:00
|
|
|
dependencies=[Depends(http_basic_or_jwt_token)],
|
2020-12-25 14:50:19 +00:00
|
|
|
)
|
2021-04-01 05:55:29 +00:00
|
|
|
app.include_router(api_backtest, prefix="/api/v1",
|
|
|
|
dependencies=[Depends(http_basic_or_jwt_token)],
|
|
|
|
)
|
2022-09-02 02:06:36 +00:00
|
|
|
app.include_router(ws_router, prefix="/api/v1",
|
|
|
|
dependencies=[Depends(get_ws_token)]
|
|
|
|
)
|
2020-12-26 07:48:15 +00:00
|
|
|
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
2021-01-10 09:31:05 +00:00
|
|
|
# UI Router MUST be last!
|
|
|
|
app.include_router(router_ui, prefix='')
|
2020-12-24 05:55:19 +00:00
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
CORSMiddleware,
|
|
|
|
allow_origins=config['api_server'].get('CORS_origins', []),
|
|
|
|
allow_credentials=True,
|
|
|
|
allow_methods=["*"],
|
|
|
|
allow_headers=["*"],
|
|
|
|
)
|
|
|
|
|
2020-12-26 16:00:30 +00:00
|
|
|
app.add_exception_handler(RPCException, self.handle_rpc_exception)
|
|
|
|
|
2022-08-29 19:41:15 +00:00
|
|
|
def start_message_queue(self):
|
2022-09-06 01:29:07 +00:00
|
|
|
if self._thread:
|
|
|
|
return
|
|
|
|
|
2022-08-29 19:41:15 +00:00
|
|
|
# Create a new loop, as it'll be just for the background thread
|
|
|
|
self._loop = asyncio.new_event_loop()
|
|
|
|
|
|
|
|
# Start the thread
|
2022-09-06 01:29:07 +00:00
|
|
|
self._thread = Thread(target=self._loop.run_forever)
|
|
|
|
self._thread.start()
|
2022-08-29 19:41:15 +00:00
|
|
|
|
|
|
|
# Finally, submit the coro to the thread
|
|
|
|
self._background_task = asyncio.run_coroutine_threadsafe(
|
|
|
|
self._broadcast_queue_data(), loop=self._loop)
|
|
|
|
|
|
|
|
async def _broadcast_queue_data(self):
|
|
|
|
# Instantiate the queue in this coroutine so it's attached to our loop
|
|
|
|
self._queue = ThreadedQueue()
|
|
|
|
async_queue = self._queue.async_q
|
|
|
|
|
|
|
|
try:
|
|
|
|
while True:
|
2022-08-31 01:21:34 +00:00
|
|
|
logger.debug("Getting queue messages...")
|
2022-08-29 19:41:15 +00:00
|
|
|
# Get data from queue
|
2022-08-31 01:21:34 +00:00
|
|
|
message = await async_queue.get()
|
|
|
|
logger.debug(f"Found message of type: {message.get('type')}")
|
2022-08-29 19:41:15 +00:00
|
|
|
# Broadcast it
|
2022-08-31 01:21:34 +00:00
|
|
|
await self._channel_manager.broadcast(message)
|
2022-08-29 19:41:15 +00:00
|
|
|
# Sleep, make this configurable?
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
except asyncio.CancelledError:
|
2022-08-31 01:21:34 +00:00
|
|
|
# Disconnect channels and stop the loop on cancel
|
|
|
|
await self._channel_manager.disconnect_all()
|
|
|
|
self._loop.stop()
|
|
|
|
|
2022-08-29 19:41:15 +00:00
|
|
|
# For testing, shouldn't happen when stable
|
|
|
|
except Exception as e:
|
2022-09-04 16:22:10 +00:00
|
|
|
logger.exception(f"Exception happened in background task: {e}")
|
2022-08-29 19:41:15 +00:00
|
|
|
|
2020-12-24 05:55:19 +00:00
|
|
|
def start_api(self):
|
|
|
|
"""
|
|
|
|
Start API ... should be run in thread.
|
|
|
|
"""
|
2020-12-27 09:56:19 +00:00
|
|
|
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")
|
|
|
|
|
|
|
|
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!")
|
|
|
|
|
2020-12-27 14:24:49 +00:00
|
|
|
if (self._config['api_server'].get('jwt_secret_key', 'super-secret')
|
|
|
|
in ('super-secret, somethingrandom')):
|
|
|
|
logger.warning("SECURITY WARNING - `jwt_secret_key` seems to be default."
|
|
|
|
"Others may be able to log into your bot.")
|
|
|
|
|
2020-12-27 09:56:19 +00:00
|
|
|
logger.info('Starting Local Rest Server.')
|
2021-01-03 06:18:41 +00:00
|
|
|
verbosity = self._config['api_server'].get('verbosity', 'error')
|
2021-06-25 16:19:29 +00:00
|
|
|
|
2020-12-24 05:55:19 +00:00
|
|
|
uvconfig = uvicorn.Config(self.app,
|
2020-12-27 09:56:19 +00:00
|
|
|
port=rest_port,
|
|
|
|
host=rest_ip,
|
2021-01-03 06:15:45 +00:00
|
|
|
use_colors=False,
|
2021-06-25 16:19:29 +00:00
|
|
|
log_config=None,
|
2020-12-29 16:23:28 +00:00
|
|
|
access_log=True if verbosity != 'error' else False,
|
|
|
|
)
|
2020-12-27 09:56:19 +00:00
|
|
|
try:
|
|
|
|
self._server = UvicornServer(uvconfig)
|
2021-03-21 09:45:44 +00:00
|
|
|
if self._standalone:
|
|
|
|
self._server.run()
|
|
|
|
else:
|
2022-09-04 16:22:10 +00:00
|
|
|
self.start_message_queue()
|
2021-03-21 09:45:44 +00:00
|
|
|
self._server.run_in_thread()
|
2020-12-27 09:56:19 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("Api server failed to start.")
|