Extract api backtest logic from ApiServer class

This commit is contained in:
Matthias
2023-05-21 09:08:52 +02:00
parent fcb75560c4
commit 5316227219
5 changed files with 68 additions and 61 deletions
+43 -43
View File
@@ -16,7 +16,7 @@ from freqtrade.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse) BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@@ -30,9 +30,9 @@ router = APIRouter()
async def api_start_backtest( # noqa: C901 async def api_start_backtest( # noqa: C901
bt_settings: BacktestRequest, background_tasks: BackgroundTasks, bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
ApiServer._bt['bt_error'] = None ApiBG._bt['bt_error'] = None
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiServer._bgtask_running: if ApiBG._bgtask_running:
raise RPCException('Bot Background task already running') raise RPCException('Bot Background task already running')
if ':' in bt_settings.strategy: if ':' in bt_settings.strategy:
@@ -63,30 +63,30 @@ async def api_start_backtest( # noqa: C901
asyncio.set_event_loop(asyncio.new_event_loop()) asyncio.set_event_loop(asyncio.new_event_loop())
try: try:
# Reload strategy # Reload strategy
lastconfig = ApiServer._bt['last_config'] lastconfig = ApiBG._bt['last_config']
strat = StrategyResolver.load_strategy(btconfig) strat = StrategyResolver.load_strategy(btconfig)
validate_config_consistency(btconfig) validate_config_consistency(btconfig)
if ( if (
not ApiServer._bt['bt'] not ApiBG._bt['bt']
or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timerange') != btconfig['timerange']
): ):
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt['bt'] = Backtesting(btconfig) ApiBG._bt['bt'] = Backtesting(btconfig)
ApiServer._bt['bt'].load_bt_data_detail() ApiBG._bt['bt'].load_bt_data_detail()
else: else:
ApiServer._bt['bt'].config = btconfig ApiBG._bt['bt'].config = btconfig
ApiServer._bt['bt'].init_backtest() ApiBG._bt['bt'].init_backtest()
# Only reload data if timeframe changed. # Only reload data if timeframe changed.
if ( if (
not ApiServer._bt['data'] not ApiBG._bt['data']
or not ApiServer._bt['timerange'] or not ApiBG._bt['timerange']
or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timerange') != btconfig['timerange']
): ):
ApiServer._bt['data'], ApiServer._bt['timerange'] = ApiServer._bt[ ApiBG._bt['data'], ApiBG._bt['timerange'] = ApiBG._bt[
'bt'].load_bt_data() 'bt'].load_bt_data()
lastconfig['timerange'] = btconfig['timerange'] lastconfig['timerange'] = btconfig['timerange']
@@ -95,27 +95,27 @@ async def api_start_backtest( # noqa: C901
lastconfig['enable_protections'] = btconfig.get('enable_protections') lastconfig['enable_protections'] = btconfig.get('enable_protections')
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
ApiServer._bt['bt'].enable_protections = btconfig.get('enable_protections', False) ApiBG._bt['bt'].enable_protections = btconfig.get('enable_protections', False)
ApiServer._bt['bt'].strategylist = [strat] ApiBG._bt['bt'].strategylist = [strat]
ApiServer._bt['bt'].results = {} ApiBG._bt['bt'].results = {}
ApiServer._bt['bt'].load_prior_backtest() ApiBG._bt['bt'].load_prior_backtest()
ApiServer._bt['bt'].abort = False ApiBG._bt['bt'].abort = False
if (ApiServer._bt['bt'].results and if (ApiBG._bt['bt'].results and
strat.get_strategy_name() in ApiServer._bt['bt'].results['strategy']): strat.get_strategy_name() in ApiBG._bt['bt'].results['strategy']):
# When previous result hash matches - reuse that result and skip backtesting. # When previous result hash matches - reuse that result and skip backtesting.
logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}')
else: else:
min_date, max_date = ApiServer._bt['bt'].backtest_one_strategy( min_date, max_date = ApiBG._bt['bt'].backtest_one_strategy(
strat, ApiServer._bt['data'], ApiServer._bt['timerange']) strat, ApiBG._bt['data'], ApiBG._bt['timerange'])
ApiServer._bt['bt'].results = generate_backtest_stats( ApiBG._bt['bt'].results = generate_backtest_stats(
ApiServer._bt['data'], ApiServer._bt['bt'].all_results, ApiBG._bt['data'], ApiBG._bt['bt'].all_results,
min_date=min_date, max_date=max_date) min_date=min_date, max_date=max_date)
if btconfig.get('export', 'none') == 'trades': if btconfig.get('export', 'none') == 'trades':
store_backtest_stats( store_backtest_stats(
btconfig['exportfilename'], ApiServer._bt['bt'].results, btconfig['exportfilename'], ApiBG._bt['bt'].results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S") datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
) )
@@ -123,13 +123,13 @@ async def api_start_backtest( # noqa: C901
except (Exception, OperationalException, DependencyException) as e: except (Exception, OperationalException, DependencyException) as e:
logger.exception(f"Backtesting caused an error: {e}") logger.exception(f"Backtesting caused an error: {e}")
ApiServer._bt['bt_error'] = str(e) ApiBG._bt['bt_error'] = str(e)
pass pass
finally: finally:
ApiServer._bgtask_running = False ApiBG._bgtask_running = False
background_tasks.add_task(run_backtest) background_tasks.add_task(run_backtest)
ApiServer._bgtask_running = True ApiBG._bgtask_running = True
return { return {
"status": "running", "status": "running",
@@ -147,18 +147,18 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
Returns Result after backtesting has been ran. Returns Result after backtesting has been ran.
""" """
from freqtrade.persistence import LocalTrade from freqtrade.persistence import LocalTrade
if ApiServer._bgtask_running: if ApiBG._bgtask_running:
return { return {
"status": "running", "status": "running",
"running": True, "running": True,
"step": (ApiServer._bt['bt'].progress.action if ApiServer._bt['bt'] "step": (ApiBG._bt['bt'].progress.action if ApiBG._bt['bt']
else str(BacktestState.STARTUP)), else str(BacktestState.STARTUP)),
"progress": ApiServer._bt['bt'].progress.progress if ApiServer._bt['bt'] else 0, "progress": ApiBG._bt['bt'].progress.progress if ApiBG._bt['bt'] else 0,
"trade_count": len(LocalTrade.trades), "trade_count": len(LocalTrade.trades),
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }
if not ApiServer._bt['bt']: if not ApiBG._bt['bt']:
return { return {
"status": "not_started", "status": "not_started",
"running": False, "running": False,
@@ -166,13 +166,13 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest not yet executed" "status_msg": "Backtest not yet executed"
} }
if ApiServer._bt['bt_error']: if ApiBG._bt['bt_error']:
return { return {
"status": "error", "status": "error",
"running": False, "running": False,
"step": "", "step": "",
"progress": 0, "progress": 0,
"status_msg": f"Backtest failed with {ApiServer._bt['bt_error']}" "status_msg": f"Backtest failed with {ApiBG._bt['bt_error']}"
} }
return { return {
@@ -181,14 +181,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
"step": "finished", "step": "finished",
"progress": 1, "progress": 1,
"backtest_result": ApiServer._bt['bt'].results, "backtest_result": ApiBG._bt['bt'].results,
} }
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"""Reset backtesting""" """Reset backtesting"""
if ApiServer._bgtask_running: if ApiBG._bgtask_running:
return { return {
"status": "running", "status": "running",
"running": True, "running": True,
@@ -196,12 +196,12 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest running", "status_msg": "Backtest running",
} }
if ApiServer._bt['bt']: if ApiBG._bt['bt']:
ApiServer._bt['bt'].cleanup() ApiBG._bt['bt'].cleanup()
del ApiServer._bt['bt'] del ApiBG._bt['bt']
ApiServer._bt['bt'] = None ApiBG._bt['bt'] = None
del ApiServer._bt['data'] del ApiBG._bt['data']
ApiServer._bt['data'] = None ApiBG._bt['data'] = None
logger.info("Backtesting reset") logger.info("Backtesting reset")
return { return {
"status": "reset", "status": "reset",
@@ -214,7 +214,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
if not ApiServer._bgtask_running: if not ApiBG._bgtask_running:
return { return {
"status": "not_running", "status": "not_running",
"running": False, "running": False,
@@ -222,7 +222,7 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
} }
ApiServer._bt['bt'].abort = True ApiBG._bt['bt'].abort = True
return { return {
"status": "stopping", "status": "stopping",
"running": False, "running": False,
+4 -3
View File
@@ -6,6 +6,7 @@ from fastapi import Depends
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import _request_id_ctx_var from freqtrade.persistence.models import _request_id_ctx_var
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer from .webserver import ApiServer
@@ -43,11 +44,11 @@ def get_api_config() -> Dict[str, Any]:
def get_exchange(config=Depends(get_config)): def get_exchange(config=Depends(get_config)):
if not ApiServer._exchange: if not ApiBG._exchange:
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
ApiServer._exchange = ExchangeResolver.load_exchange( ApiBG._exchange = ExchangeResolver.load_exchange(
config, load_leverage_tiers=False) config, load_leverage_tiers=False)
return ApiServer._exchange return ApiBG._exchange
def get_message_stream(): def get_message_stream():
+2 -13
View File
@@ -1,6 +1,6 @@
import logging import logging
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Dict, Optional from typing import Any, Optional
import orjson import orjson
import uvicorn import uvicorn
@@ -36,19 +36,8 @@ class ApiServer(RPCHandler):
__initialized = False __initialized = False
_rpc: RPC _rpc: RPC
# Backtesting type: Backtesting
_bt: Dict[str, Any] = {
'bt': None,
'data': None,
'timerange': None,
'last_config': {},
'bt_error': None,
}
_has_rpc: bool = False _has_rpc: bool = False
_bgtask_running: bool = False
_config: Config = {} _config: Config = {}
# Exchange - only available in webserver mode.
_exchange = None
# websocket message stuff # websocket message stuff
_message_stream: Optional[MessageStream] = None _message_stream: Optional[MessageStream] = None
@@ -85,7 +74,7 @@ class ApiServer(RPCHandler):
""" """
Attach rpc handler Attach rpc handler
""" """
if not self._has_rpc: if not ApiServer._has_rpc:
ApiServer._rpc = rpc ApiServer._rpc = rpc
ApiServer._has_rpc = True ApiServer._has_rpc = True
else: else:
@@ -0,0 +1,16 @@
from typing import Any, Dict
class ApiBG():
# Backtesting type: Backtesting
_bt: Dict[str, Any] = {
'bt': None,
'data': None,
'timerange': None,
'last_config': {},
'bt_error': None,
}
_bgtask_running: bool = False
# Exchange - only available in webserver mode.
_exchange = None
+3 -2
View File
@@ -26,6 +26,7 @@ from freqtrade.rpc import RPC
from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro,
get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) get_patched_freqtradebot, log_has, log_has_re, patch_get_signal)
@@ -1733,7 +1734,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
assert result['status_msg'] == 'Backtest ended' assert result['status_msg'] == 'Backtest ended'
# Simulate running backtest # Simulate running backtest
ApiServer._bgtask_running = True ApiBG._bgtask_running = True
rc = client_get(client, f"{BASE_URI}/backtest/abort") rc = client_get(client, f"{BASE_URI}/backtest/abort")
assert_response(rc) assert_response(rc)
result = rc.json() result = rc.json()
@@ -1762,7 +1763,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
result = rc.json() result = rc.json()
assert 'Bot Background task already running' in result['error'] assert 'Bot Background task already running' in result['error']
ApiServer._bgtask_running = False ApiBG._bgtask_running = False
# Rerun backtest (should get previous result) # Rerun backtest (should get previous result)
rc = client_post(client, f"{BASE_URI}/backtest", data=data) rc = client_post(client, f"{BASE_URI}/backtest", data=data)