Merge pull request #5243 from freqtrade/feat/webservermode_progress
Introduce webserver mode subcommand
This commit is contained in:
176
freqtrade/rpc/api_server/api_backtest.py
Normal file
176
freqtrade/rpc/api_server/api_backtest.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
|
||||
from freqtrade.enums import BacktestState
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||
from freqtrade.rpc.api_server.deps import get_config
|
||||
from freqtrade.rpc.api_server.webserver import ApiServer
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Private API, protected by authentication
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
|
||||
config=Depends(get_config)):
|
||||
"""Start backtesting if not done so already"""
|
||||
if ApiServer._bgtask_running:
|
||||
raise RPCException('Bot Background task already running')
|
||||
|
||||
btconfig = deepcopy(config)
|
||||
settings = dict(bt_settings)
|
||||
# Pydantic models will contain all keys, but non-provided ones are None
|
||||
for setting in settings.keys():
|
||||
if settings[setting] is not None:
|
||||
btconfig[setting] = settings[setting]
|
||||
|
||||
# Start backtesting
|
||||
# Initialize backtesting object
|
||||
def run_backtest():
|
||||
from freqtrade.optimize.optimize_reports import generate_backtest_stats
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
try:
|
||||
# Reload strategy
|
||||
lastconfig = ApiServer._bt_last_config
|
||||
strat = StrategyResolver.load_strategy(btconfig)
|
||||
|
||||
if (
|
||||
not ApiServer._bt
|
||||
or lastconfig.get('timeframe') != strat.timeframe
|
||||
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
|
||||
):
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
ApiServer._bt = Backtesting(btconfig)
|
||||
|
||||
# Only reload data if timeframe or timerange changed.
|
||||
if (
|
||||
not ApiServer._bt_data
|
||||
or not ApiServer._bt_timerange
|
||||
or lastconfig.get('timerange') != btconfig['timerange']
|
||||
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
|
||||
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
|
||||
or lastconfig.get('protections') != btconfig.get('protections', [])
|
||||
or lastconfig.get('timeframe') != strat.timeframe
|
||||
):
|
||||
lastconfig['timerange'] = btconfig['timerange']
|
||||
lastconfig['protections'] = btconfig.get('protections', [])
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
lastconfig['timeframe'] = strat.timeframe
|
||||
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
||||
|
||||
ApiServer._bt.abort = False
|
||||
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
||||
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
||||
ApiServer._bt.results = generate_backtest_stats(
|
||||
ApiServer._bt_data, ApiServer._bt.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
logger.info("Backtest finished.")
|
||||
|
||||
except DependencyException as e:
|
||||
logger.info(f"Backtesting caused an error: {e}")
|
||||
pass
|
||||
finally:
|
||||
ApiServer._bgtask_running = False
|
||||
|
||||
background_tasks.add_task(run_backtest)
|
||||
ApiServer._bgtask_running = True
|
||||
|
||||
return {
|
||||
"status": "running",
|
||||
"running": True,
|
||||
"progress": 0,
|
||||
"step": str(BacktestState.STARTUP),
|
||||
"status_msg": "Backtest started",
|
||||
}
|
||||
|
||||
|
||||
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
def api_get_backtest():
|
||||
"""
|
||||
Get backtesting result.
|
||||
Returns Result after backtesting has been ran.
|
||||
"""
|
||||
from freqtrade.persistence import LocalTrade
|
||||
if ApiServer._bgtask_running:
|
||||
return {
|
||||
"status": "running",
|
||||
"running": True,
|
||||
"step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP),
|
||||
"progress": ApiServer._bt.progress.progress if ApiServer._bt else 0,
|
||||
"trade_count": len(LocalTrade.trades),
|
||||
"status_msg": "Backtest running",
|
||||
}
|
||||
|
||||
if not ApiServer._bt:
|
||||
return {
|
||||
"status": "not_started",
|
||||
"running": False,
|
||||
"step": "",
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest not yet executed"
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "ended",
|
||||
"running": False,
|
||||
"status_msg": "Backtest ended",
|
||||
"step": "finished",
|
||||
"progress": 1,
|
||||
"backtest_result": ApiServer._bt.results,
|
||||
}
|
||||
|
||||
|
||||
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
def api_delete_backtest():
|
||||
"""Reset backtesting"""
|
||||
if ApiServer._bgtask_running:
|
||||
return {
|
||||
"status": "running",
|
||||
"running": True,
|
||||
"step": "",
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest running",
|
||||
}
|
||||
if ApiServer._bt:
|
||||
del ApiServer._bt
|
||||
ApiServer._bt = None
|
||||
del ApiServer._bt_data
|
||||
ApiServer._bt_data = None
|
||||
logger.info("Backtesting reset")
|
||||
return {
|
||||
"status": "reset",
|
||||
"running": False,
|
||||
"step": "",
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest reset",
|
||||
}
|
||||
|
||||
|
||||
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
def api_backtest_abort():
|
||||
if not ApiServer._bgtask_running:
|
||||
return {
|
||||
"status": "not_running",
|
||||
"running": False,
|
||||
"step": "",
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest ended",
|
||||
}
|
||||
ApiServer._bt.abort = True
|
||||
return {
|
||||
"status": "stopping",
|
||||
"running": False,
|
||||
"step": "",
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest ended",
|
||||
}
|
@@ -123,17 +123,17 @@ class ShowConfig(BaseModel):
|
||||
stake_currency_decimals: int
|
||||
max_open_trades: int
|
||||
minimal_roi: Dict[str, Any]
|
||||
stoploss: float
|
||||
trailing_stop: bool
|
||||
stoploss: Optional[float]
|
||||
trailing_stop: Optional[bool]
|
||||
trailing_stop_positive: Optional[float]
|
||||
trailing_stop_positive_offset: Optional[float]
|
||||
trailing_only_offset_is_reached: Optional[bool]
|
||||
use_custom_stoploss: Optional[bool]
|
||||
timeframe: str
|
||||
timeframe: Optional[str]
|
||||
timeframe_ms: int
|
||||
timeframe_min: int
|
||||
exchange: str
|
||||
strategy: str
|
||||
strategy: Optional[str]
|
||||
forcebuy_enabled: bool
|
||||
ask_strategy: Dict[str, Any]
|
||||
bid_strategy: Dict[str, Any]
|
||||
@@ -318,3 +318,24 @@ class PairHistory(BaseModel):
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
|
||||
}
|
||||
|
||||
|
||||
class BacktestRequest(BaseModel):
|
||||
strategy: str
|
||||
timeframe: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
stake_amount: Optional[Union[float, str]]
|
||||
enable_protections: bool
|
||||
dry_run_wallet: Optional[float]
|
||||
|
||||
|
||||
class BacktestResponse(BaseModel):
|
||||
status: str
|
||||
running: bool
|
||||
status_msg: str
|
||||
step: str
|
||||
progress: float
|
||||
trade_count: Optional[float]
|
||||
# TODO: Properly type backtestresult...
|
||||
backtest_result: Optional[Dict[str, Any]]
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
@@ -22,6 +23,8 @@ from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
# Private API, protected by authentication
|
||||
@@ -249,7 +252,7 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
|
||||
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
||||
|
||||
pairs = list({x[0] for x in pair_interval})
|
||||
|
||||
pairs.sort()
|
||||
result = {
|
||||
'length': len(pairs),
|
||||
'pairs': pairs,
|
||||
|
@@ -8,6 +8,7 @@ from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
||||
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
||||
|
||||
@@ -28,17 +29,37 @@ class FTJSONResponse(JSONResponse):
|
||||
|
||||
class ApiServer(RPCHandler):
|
||||
|
||||
__instance = None
|
||||
__initialized = False
|
||||
|
||||
_rpc: RPC
|
||||
# Backtesting type: Backtesting
|
||||
_bt = None
|
||||
_bt_data = None
|
||||
_bt_timerange = None
|
||||
_bt_last_config: Dict[str, Any] = {}
|
||||
_has_rpc: bool = False
|
||||
_bgtask_running: bool = False
|
||||
_config: Dict[str, Any] = {}
|
||||
|
||||
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
||||
super().__init__(rpc, config)
|
||||
self._server = None
|
||||
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
|
||||
|
||||
ApiServer._rpc = rpc
|
||||
ApiServer._has_rpc = True
|
||||
def __init__(self, config: Dict[str, Any], standalone: bool = False) -> None:
|
||||
ApiServer._config = config
|
||||
if self.__initialized and (standalone or self._standalone):
|
||||
return
|
||||
self._standalone: bool = standalone
|
||||
self._server = None
|
||||
ApiServer.__initialized = True
|
||||
|
||||
api_config = self._config['api_server']
|
||||
|
||||
self.app = FastAPI(title="Freqtrade API",
|
||||
@@ -50,12 +71,33 @@ class ApiServer(RPCHandler):
|
||||
|
||||
self.start_api()
|
||||
|
||||
def add_rpc_handler(self, rpc: RPC):
|
||||
"""
|
||||
Attach rpc handler
|
||||
"""
|
||||
if not self._has_rpc:
|
||||
ApiServer._rpc = rpc
|
||||
ApiServer._has_rpc = True
|
||||
else:
|
||||
# This should not happen assuming we didn't mess up.
|
||||
raise OperationalException('RPC Handler already attached.')
|
||||
|
||||
def cleanup(self) -> None:
|
||||
""" Cleanup pending module resources """
|
||||
if self._server:
|
||||
ApiServer._has_rpc = False
|
||||
del ApiServer._rpc
|
||||
if self._server and not self._standalone:
|
||||
logger.info("Stopping API Server")
|
||||
self._server.cleanup()
|
||||
|
||||
@classmethod
|
||||
def shutdown(cls):
|
||||
cls.__initialized = False
|
||||
del cls.__instance
|
||||
cls.__instance = None
|
||||
cls._has_rpc = False
|
||||
cls._rpc = None
|
||||
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
pass
|
||||
|
||||
@@ -68,6 +110,7 @@ class ApiServer(RPCHandler):
|
||||
|
||||
def configure_app(self, app: FastAPI, config):
|
||||
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
|
||||
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
|
||||
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
|
||||
from freqtrade.rpc.api_server.web_ui import router_ui
|
||||
@@ -77,6 +120,9 @@ class ApiServer(RPCHandler):
|
||||
app.include_router(api_v1, prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token)],
|
||||
)
|
||||
app.include_router(api_backtest, prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token)],
|
||||
)
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||
# UI Router MUST be last!
|
||||
app.include_router(router_ui, prefix='')
|
||||
@@ -125,6 +171,9 @@ class ApiServer(RPCHandler):
|
||||
)
|
||||
try:
|
||||
self._server = UvicornServer(uvconfig)
|
||||
self._server.run_in_thread()
|
||||
if self._standalone:
|
||||
self._server.run()
|
||||
else:
|
||||
self._server.run_in_thread()
|
||||
except Exception:
|
||||
logger.exception("Api server failed to start.")
|
||||
|
@@ -119,9 +119,9 @@ class RPC:
|
||||
'bot_name': config.get('bot_name', 'freqtrade'),
|
||||
'timeframe': config.get('timeframe'),
|
||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||
) if 'timeframe' in config else '',
|
||||
) if 'timeframe' in config else 0,
|
||||
'timeframe_min': timeframe_to_minutes(config['timeframe']
|
||||
) if 'timeframe' in config else '',
|
||||
) if 'timeframe' in config else 0,
|
||||
'exchange': config['exchange']['name'],
|
||||
'strategy': config['strategy'],
|
||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||
|
@@ -36,15 +36,16 @@ class RPCManager:
|
||||
if config.get('api_server', {}).get('enabled', False):
|
||||
logger.info('Enabling rpc.api_server')
|
||||
from freqtrade.rpc.api_server import ApiServer
|
||||
|
||||
self.registered_modules.append(ApiServer(self._rpc, config))
|
||||
apiserver = ApiServer(config)
|
||||
apiserver.add_rpc_handler(self._rpc)
|
||||
self.registered_modules.append(apiserver)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
""" Stops all enabled rpc modules """
|
||||
logger.info('Cleaning up rpc modules ...')
|
||||
while self.registered_modules:
|
||||
mod = self.registered_modules.pop()
|
||||
logger.debug('Cleaning up rpc.%s ...', mod.name)
|
||||
logger.info('Cleaning up rpc.%s ...', mod.name)
|
||||
mod.cleanup()
|
||||
del mod
|
||||
|
||||
|
Reference in New Issue
Block a user