From ef137546fe1f1ed536f7daafcb1021d091a3f292 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 May 2020 19:57:17 +0200 Subject: [PATCH 01/31] Add webserver entrypoint --- freqtrade/commands/__init__.py | 2 +- freqtrade/commands/arguments.py | 11 ++++++++++- freqtrade/commands/trade_commands.py | 8 ++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 784b99bed..a13d0ba49 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -19,4 +19,4 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_h from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit -from freqtrade.commands.trade_commands import start_trading +from freqtrade.commands.trade_commands import start_trading, start_webserver diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index ba37237f6..1efe450ff 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -16,6 +16,8 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] +ARGS_WEBSERVER: List[str] = [] + ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee", "pairs"] @@ -176,7 +178,8 @@ class Arguments: start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_hyperopt, start_new_strategy, start_plot_dataframe, start_plot_profit, - start_show_trades, start_test_pairlist, start_trading) + start_show_trades, start_test_pairlist, start_trading, + start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added @@ -384,3 +387,9 @@ class Arguments: ) plot_profit_cmd.set_defaults(func=start_plot_profit) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) + + # Add webserver subcommand + webserver_cmd = subparsers.add_parser('webserver', help='Webserver module.', + parents=[_common_parser, _strategy_parser]) + webserver_cmd.set_defaults(func=start_webserver) + self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 535844844..69ea5998f 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -27,3 +27,11 @@ def start_trading(args: Dict[str, Any]) -> int: logger.info("worker found ... calling exit") worker.exit() return 0 + + +def start_webserver(args: Dict[str, Any]) -> int: + """ + Main entry point for webserver mode + """ + + print(args) From 97e8ec91f0c469808ac0f7f9e9880f73ffed50a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 May 2020 10:44:43 +0200 Subject: [PATCH 02/31] Save configuration file paths --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 1d2e3f802..46e6f4e36 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -71,7 +71,7 @@ class Configuration: # Merge config options, overwriting old values config = deep_merge_dicts(load_config_file(path), config) - + config['config_files'] = files # Normalize config if 'internals' not in config: config['internals'] = {} From 800e314bfda53b6ccac6ca36f6a13fbb44d3d1f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 19:46:54 +0100 Subject: [PATCH 03/31] Store backtesting results in backtest instance --- freqtrade/optimize/backtesting.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7c6b7cbc3..f883c0cbf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -57,6 +57,7 @@ class Backtesting: LoggingMixin.show_output = False self.config = config + self.results: Optional[Dict[str, Any]] = None # Reset keys for backtesting remove_credentials(self.config) @@ -537,11 +538,12 @@ class Backtesting: for strat in self.strategylist: min_date, max_date = self.backtest_one_strategy(strat, data, timerange) if len(self.strategylist) > 0: - stats = generate_backtest_stats(data, self.all_results, - min_date=min_date, max_date=max_date) + + self.results = generate_backtest_stats(data, self.all_results, + min_date=min_date, max_date=max_date) if self.config.get('export', 'none') == 'trades': - store_backtest_stats(self.config['exportfilename'], stats) + store_backtest_stats(self.config['exportfilename'], self.results) # Show backtest results - show_backtest_results(self.config, stats) + show_backtest_results(self.config, self.results) From 02b84bd0188df372a1346a95e6c84b6cc2b4b2c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 20:02:27 +0100 Subject: [PATCH 04/31] Introduce webserver mode for fastapi --- freqtrade/commands/trade_commands.py | 10 +++++--- freqtrade/enums/runmode.py | 1 + freqtrade/rpc/api_server/webserver.py | 35 ++++++++++++++++++++++++--- freqtrade/rpc/rpc_manager.py | 7 +++--- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 69ea5998f..ca1451778 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -1,7 +1,6 @@ import logging from typing import Any, Dict - logger = logging.getLogger(__name__) @@ -29,9 +28,14 @@ def start_trading(args: Dict[str, Any]) -> int: return 0 -def start_webserver(args: Dict[str, Any]) -> int: +def start_webserver(args: Dict[str, Any]) -> None: """ Main entry point for webserver mode """ + from freqtrade.rpc.api_server import ApiServer + from freqtrade.configuration import Configuration + from freqtrade.enums import RunMode - print(args) + # Initialize configuration + config = Configuration(args, RunMode.WEBSERVER).get_config() + ApiServer(config, standalone=True) diff --git a/freqtrade/enums/runmode.py b/freqtrade/enums/runmode.py index 7826d1d0c..6545aaec7 100644 --- a/freqtrade/enums/runmode.py +++ b/freqtrade/enums/runmode.py @@ -14,6 +14,7 @@ class RunMode(Enum): UTIL_EXCHANGE = "util_exchange" UTIL_NO_EXCHANGE = "util_no_exchange" PLOT = "plot" + WEBSERVER = "webserver" OTHER = "other" diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index a43d4abe6..9cbd5512a 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -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,16 +29,30 @@ class FTJSONResponse(JSONResponse): class ApiServer(RPCHandler): + __instance = None + __initialized = False + _rpc: RPC _has_rpc: bool = False _config: Dict[str, Any] = {} - def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: - super().__init__(rpc, config) + 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 + + def __init__(self, config: Dict[str, Any], standalone: bool = False) -> None: + if self.__initialized and (standalone or self._standalone): + return + self._standalone: bool = standalone + self._config = config self._server = None - ApiServer._rpc = rpc - ApiServer._has_rpc = True ApiServer._config = config api_config = self._config['api_server'] @@ -50,6 +65,18 @@ class ApiServer(RPCHandler): self.start_api() + def add_rpc_handler(self, rpc: RPC): + """ + Attach rpc handler + """ + if not self._rpc: + self._rpc = 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: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 18ed68041..8814f70a0 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -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 From df55259737608a304c2fddf545bed5a8d45434f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 14:18:15 +0100 Subject: [PATCH 05/31] Add start_trading endpoint --- freqtrade/rpc/api_server/api_v1.py | 1 + freqtrade/rpc/api_server/webserver.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 965664028..dd1df6353 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -256,3 +256,4 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option 'pair_interval': pair_interval, } return result + diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 9cbd5512a..f53c8b0c6 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -34,6 +34,7 @@ class ApiServer(RPCHandler): _rpc: RPC _has_rpc: bool = False + _bgtask_running: bool = False _config: Dict[str, Any] = {} def __new__(cls, *args, **kwargs): @@ -47,13 +48,13 @@ class ApiServer(RPCHandler): return ApiServer.__instance 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._config = config self._server = None + ApiServer.__initialized = True - ApiServer._config = config api_config = self._config['api_server'] self.app = FastAPI(title="Freqtrade API", @@ -69,7 +70,7 @@ class ApiServer(RPCHandler): """ Attach rpc handler """ - if not self._rpc: + if not self._has_rpc: self._rpc = rpc ApiServer._rpc = rpc ApiServer._has_rpc = True From 5c18c8726d18006c770cbd035d574a06b1ad6521 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 15:13:32 +0100 Subject: [PATCH 06/31] Implement backtesting with fastapi --- freqtrade/rpc/api_server/api_schemas.py | 16 ++++ freqtrade/rpc/api_server/api_v1.py | 104 ++++++++++++++++++++++-- freqtrade/rpc/api_server/webserver.py | 2 + 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a0f1c05a6..eccfd8157 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -313,3 +313,19 @@ 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[int] + + +class BacktestResponse(BaseModel): + status: str + running: bool + status_msg: str + # TODO: Properly type backtestresult... + backtest_result: Optional[Dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index dd1df6353..9166a1cee 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -1,8 +1,10 @@ +import asyncio +import logging from copy import deepcopy from pathlib import Path from typing import List, Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, BackgroundTasks, Depends from fastapi.exceptions import HTTPException from freqtrade import __version__ @@ -10,18 +12,21 @@ from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC -from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, - DeleteLockRequest, DeleteTrade, ForceBuyPayload, - ForceBuyResponse, ForceSellPayload, Locks, Logs, - OpenTradeSchema, PairHistory, PerformanceEntry, - Ping, PlotConfig, Profit, ResultMsg, ShowConfig, - Stats, StatusMsg, StrategyListResponse, - StrategyResponse, Version, WhitelistResponse) +from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestRequest, BacktestResponse, + Balances, BlacklistPayload, BlacklistResponse, + Count, Daily, DeleteLockRequest, DeleteTrade, + ForceBuyPayload, ForceBuyResponse, + ForceSellPayload, Locks, Logs, OpenTradeSchema, + PairHistory, PerformanceEntry, Ping, PlotConfig, + Profit, ResultMsg, ShowConfig, Stats, StatusMsg, + StrategyListResponse, StrategyResponse, Version, + WhitelistResponse) 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 @@ -257,3 +262,84 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option } return result + +@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.backtesting import Backtesting + asyncio.set_event_loop(asyncio.new_event_loop()) + try: + ApiServer._backtesting = Backtesting(btconfig) + ApiServer._backtesting.start() + finally: + ApiServer._bgtask_running = False + + background_tasks.add_task(run_backtest) + ApiServer._bgtask_running = True + + return { + "status": "running", + "running": True, + "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. + """ + if not ApiServer._backtesting: + return { + "status": "not_started", + "running": False, + "status_msg": "Backtesting not yet executed" + } + if ApiServer._bgtask_running: + return { + "status": "running", + "running": True, + "status_msg": "Backtest running", + } + + return { + "status": "ended", + "running": False, + "status_msg": "Backtest ended", + "backtest_result": ApiServer._backtesting.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, + "status_msg": "Backtest running", + } + if ApiServer._backtesting: + del ApiServer._backtesting + ApiServer._backtesting = None + logger.info("Backtesting reset") + return { + "status": "reset", + "running": False, + "status_msg": "Backtesting reset", + } diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index f53c8b0c6..f2ebf785f 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -33,6 +33,8 @@ class ApiServer(RPCHandler): __initialized = False _rpc: RPC + # Backtesting type: Backtesting + _backtesting = None _has_rpc: bool = False _bgtask_running: bool = False _config: Dict[str, Any] = {} From edb8c4f0e5898fe159ef716b468d6cbd5e97843a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 16:12:10 +0100 Subject: [PATCH 07/31] Fix tests for webserver mode --- freqtrade/rpc/api_server/webserver.py | 8 ++++++++ tests/rpc/test_rpc_apiserver.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index f2ebf785f..f3905fd8f 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -86,6 +86,14 @@ class ApiServer(RPCHandler): 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 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 89da68da7..a8ffd3eff 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -48,9 +48,13 @@ def botclient(default_conf, mocker): ftbot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(ftbot) mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', MagicMock()) - apiserver = ApiServer(rpc, default_conf) - yield ftbot, TestClient(apiserver.app) - # Cleanup ... ? + try: + apiserver = ApiServer(default_conf) + apiserver.add_rpc_handler(rpc) + yield ftbot, TestClient(apiserver.app) + # Cleanup ... ? + finally: + ApiServer.shutdown() def client_post(client, url, data={}): @@ -235,8 +239,10 @@ def test_api__init__(default_conf, mocker): }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) - apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + apiserver = ApiServer(default_conf) + apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) assert apiserver._config == default_conf + ApiServer.shutdown() def test_api_UvicornServer(mocker): @@ -301,7 +307,8 @@ def test_api_run(default_conf, mocker, caplog): server_mock = MagicMock() mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock) - apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + apiserver = ApiServer(default_conf) + apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) assert server_mock.call_count == 1 assert apiserver._config == default_conf @@ -344,6 +351,7 @@ def test_api_run(default_conf, mocker, caplog): MagicMock(side_effect=Exception)) apiserver.start_api() assert log_has("Api server failed to start.", caplog) + ApiServer.shutdown() def test_api_cleanup(default_conf, mocker, caplog): @@ -359,11 +367,13 @@ def test_api_cleanup(default_conf, mocker, caplog): server_mock.cleanup = MagicMock() mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock) - apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + apiserver = ApiServer(default_conf) + apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) apiserver.cleanup() assert apiserver._server.cleanup.call_count == 1 assert log_has("Stopping API Server", caplog) + ApiServer.shutdown() def test_api_reloadconf(botclient): From f96d7dfe6dbf78b11c878749e49b01b27a9af7e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 15:05:54 +0100 Subject: [PATCH 08/31] Allow backtesting to reuse data Allow activating / deactivating protections dynamically --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 58 +++++++++++++++++++------ freqtrade/rpc/api_server/webserver.py | 5 ++- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eccfd8157..bb611f287 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -321,6 +321,7 @@ class BacktestRequest(BaseModel): timerange: Optional[str] max_open_trades: Optional[int] stake_amount: Optional[int] + enable_protections: bool class BacktestResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 9166a1cee..c162a57dc 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -280,11 +280,40 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac # Start backtesting # Initialize backtesting object def run_backtest(): - from freqtrade.optimize.backtesting import Backtesting + from freqtrade.optimize.optimize_reports import generate_backtest_stats + from freqtrade.resolvers import StrategyResolver asyncio.set_event_loop(asyncio.new_event_loop()) try: - ApiServer._backtesting = Backtesting(btconfig) - ApiServer._backtesting.start() + # Reload strategy + strat = StrategyResolver.load_strategy(btconfig) + + if (not ApiServer._bt + or ApiServer._lastbacktestconfig.get('timeframe') != strat.timeframe + or ApiServer._lastbacktestconfig.get('enable_protections') != btconfig.get('enable_protections') + or ApiServer._lastbacktestconfig.get('protections') != btconfig.get('protections', [])): + # TODO: Investigate if enabling protections can be dynamically ingested from here... + from freqtrade.optimize.backtesting import Backtesting + ApiServer._bt = Backtesting(btconfig) + # Reset data if backtesting is reloaded + # TODO: is this always necessary?? + ApiServer._backtestdata = None + + if (not ApiServer._backtestdata or not ApiServer._bt_timerange + or ApiServer._lastbacktestconfig.get('timerange') != btconfig['timerange']): + ApiServer._lastbacktestconfig['timerange'] = btconfig['timerange'] + ApiServer._lastbacktestconfig['protections'] = btconfig.get('protections', []) + ApiServer._lastbacktestconfig['enable_protections'] = btconfig.get('enable_protections') + ApiServer._lastbacktestconfig['timeframe'] = strat.timeframe + ApiServer._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() + + min_date, max_date = ApiServer._bt.backtest_one_strategy( + strat, ApiServer._backtestdata, + ApiServer._bt_timerange) + ApiServer._bt.results = generate_backtest_stats( + ApiServer._backtestdata, ApiServer._bt.all_results, + min_date=min_date, max_date=max_date) + logger.info("Backtesting finished.") + finally: ApiServer._bgtask_running = False @@ -304,12 +333,6 @@ def api_get_backtest(): Get backtesting result. Returns Result after backtesting has been ran. """ - if not ApiServer._backtesting: - return { - "status": "not_started", - "running": False, - "status_msg": "Backtesting not yet executed" - } if ApiServer._bgtask_running: return { "status": "running", @@ -317,11 +340,18 @@ def api_get_backtest(): "status_msg": "Backtest running", } + if not ApiServer._bt: + return { + "status": "not_started", + "running": False, + "status_msg": "Backtesting not yet executed" + } + return { "status": "ended", "running": False, "status_msg": "Backtest ended", - "backtest_result": ApiServer._backtesting.results, + "backtest_result": ApiServer._bt.results, } @@ -334,9 +364,11 @@ def api_delete_backtest(): "running": True, "status_msg": "Backtest running", } - if ApiServer._backtesting: - del ApiServer._backtesting - ApiServer._backtesting = None + if ApiServer._bt: + del ApiServer._bt + ApiServer._bt = None + del ApiServer._backtestdata + ApiServer._backtestdata = None logger.info("Backtesting reset") return { "status": "reset", diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index f3905fd8f..eccbadcb3 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -34,7 +34,10 @@ class ApiServer(RPCHandler): _rpc: RPC # Backtesting type: Backtesting - _backtesting = None + _bt = None + _backtestdata = None + _bt_timerange = None + _lastbacktestconfig: Dict[str, Any] = {} _has_rpc: bool = False _bgtask_running: bool = False _config: Dict[str, Any] = {} From 06b6726029d80903e52d6aed2c4e7599b106de12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 09:56:13 +0100 Subject: [PATCH 09/31] Support compounding key --- freqtrade/rpc/api_server/api_schemas.py | 3 ++- freqtrade/rpc/api_server/api_v1.py | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bb611f287..bf5382131 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -320,8 +320,9 @@ class BacktestRequest(BaseModel): timeframe: Optional[str] timerange: Optional[str] max_open_trades: Optional[int] - stake_amount: Optional[int] + stake_amount: Optional[Union[float, str]] enable_protections: bool + dry_run_wallet: Optional[float] class BacktestResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index c162a57dc..d4f138ee8 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -285,12 +285,15 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy + lastconfig = ApiServer._lastbacktestconfig strat = StrategyResolver.load_strategy(btconfig) if (not ApiServer._bt - or ApiServer._lastbacktestconfig.get('timeframe') != strat.timeframe - or ApiServer._lastbacktestconfig.get('enable_protections') != btconfig.get('enable_protections') - or ApiServer._lastbacktestconfig.get('protections') != btconfig.get('protections', [])): + or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('enable_protections') != btconfig.get('enable_protections') + or lastconfig.get('protections') != btconfig.get('protections', []) + or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0) + ): # TODO: Investigate if enabling protections can be dynamically ingested from here... from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) @@ -299,11 +302,12 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac ApiServer._backtestdata = None if (not ApiServer._backtestdata or not ApiServer._bt_timerange - or ApiServer._lastbacktestconfig.get('timerange') != btconfig['timerange']): - ApiServer._lastbacktestconfig['timerange'] = btconfig['timerange'] - ApiServer._lastbacktestconfig['protections'] = btconfig.get('protections', []) - ApiServer._lastbacktestconfig['enable_protections'] = btconfig.get('enable_protections') - ApiServer._lastbacktestconfig['timeframe'] = strat.timeframe + or lastconfig.get('timerange') != btconfig['timerange']): + 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._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() min_date, max_date = ApiServer._bt.backtest_one_strategy( From 048008756ff8390c34f32a583bef05d1205e079d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Mar 2021 19:16:18 +0100 Subject: [PATCH 10/31] Add progress tracking for backtesting --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/backteststate.py | 14 +++++++++++++ freqtrade/optimize/backtesting.py | 28 ++++++++++++++++++++++++- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/api_server/api_v1.py | 14 +++++++++++-- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 freqtrade/enums/backteststate.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 78163d86f..ac5f804c9 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 +from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/backteststate.py b/freqtrade/enums/backteststate.py new file mode 100644 index 000000000..4c1bd7cbc --- /dev/null +++ b/freqtrade/enums/backteststate.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class BacktestState(Enum): + """ + Bot application states + """ + STARTUP = 1 + DATALOAD = 2 + ANALYZE = 3 + BACKTEST = 4 + + def __str__(self): + return f"{self.name.lower()}" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f883c0cbf..9168a0ddb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType +from freqtrade.enums import BacktestState, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin @@ -118,11 +118,26 @@ class Backtesting: # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) + self._progress = 0 + self._max_steps = 0 + self._action = BacktestState.STARTUP + def __del__(self): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True + def set_progress(self, action: BacktestState, progress: float): + self._progress = 0 + self._action = action + + def get_progress(self) -> float: + + return round(self._progress / self._max_steps, 5) if self._max_steps > 0 else 0 + + def get_action(self) -> str: + return str(self._action) + def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting @@ -145,6 +160,8 @@ class Backtesting: Loads backtest data and returns the data combined with the timerange as tuple. """ + self.set_progress(BacktestState.DATALOAD, 0) + timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -168,6 +185,7 @@ class Backtesting: timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) + self.set_progress(BacktestState.DATALOAD, 1) return data, timerange def prepare_backtest(self, enable_protections): @@ -404,6 +422,9 @@ class Backtesting: open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 + self.set_progress(BacktestState.BACKTEST, 0) + self._max_steps = int((end_date - start_date) / timedelta(minutes=self.timeframe_min)) + # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: open_trade_count_start = open_trade_count @@ -463,6 +484,7 @@ class Backtesting: self.protections.global_stop(tmp) # Move time one configured time_interval ahead. + self._progress += 1 tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) @@ -478,6 +500,8 @@ class Backtesting: } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): + self.set_progress(BacktestState.ANALYZE, 0) + self._max_steps = 0 logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) @@ -504,6 +528,8 @@ class Backtesting: "No data left after adjusting for startup candles.") min_date, max_date = history.get_timerange(preprocessed) + self.set_progress(BacktestState.BACKTEST, 0) + logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days).') diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bf5382131..9a4ac5cd0 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -329,5 +329,7 @@ class BacktestResponse(BaseModel): status: str running: bool status_msg: str + step: str + progress: float # TODO: Properly type backtestresult... backtest_result: Optional[Dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index d4f138ee8..30b4c998c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -23,6 +23,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestReques WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException +from freqtrade.state import BacktestState logger = logging.getLogger(__name__) @@ -292,8 +293,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('enable_protections') != btconfig.get('enable_protections') or lastconfig.get('protections') != btconfig.get('protections', []) - or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0) - ): + or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): # TODO: Investigate if enabling protections can be dynamically ingested from here... from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) @@ -327,6 +327,8 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac return { "status": "running", "running": True, + "progress": 0, + "step": str(BacktestState.STARTUP), "status_msg": "Backtest started", } @@ -341,6 +343,8 @@ def api_get_backtest(): return { "status": "running", "running": True, + "step": ApiServer._bt.get_action() if ApiServer._bt else str(BacktestState.STARTUP), + "progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, "status_msg": "Backtest running", } @@ -348,6 +352,8 @@ def api_get_backtest(): return { "status": "not_started", "running": False, + "step": "", + "progress": 0, "status_msg": "Backtesting not yet executed" } @@ -355,6 +361,8 @@ def api_get_backtest(): "status": "ended", "running": False, "status_msg": "Backtest ended", + "step": "finished", + "progress": 1, "backtest_result": ApiServer._bt.results, } @@ -366,6 +374,7 @@ def api_delete_backtest(): return { "status": "running", "running": True, + "progress": 0, "status_msg": "Backtest running", } if ApiServer._bt: @@ -377,5 +386,6 @@ def api_delete_backtest(): return { "status": "reset", "running": False, + "progress": 0, "status_msg": "Backtesting reset", } From 37b15e830a6b67ba5b7b89a2d393c650568bf82c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Mar 2021 19:11:17 +0100 Subject: [PATCH 11/31] Add trade count to progress --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 9a4ac5cd0..f10c501f7 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -331,5 +331,6 @@ class BacktestResponse(BaseModel): status_msg: str step: str progress: float + trade_count: Optional[float] # TODO: Properly type backtestresult... backtest_result: Optional[Dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 30b4c998c..339f8565a 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -339,12 +339,14 @@ def api_get_backtest(): Get backtesting result. Returns Result after backtesting has been ran. """ + from freqtrade.persistence import Trade if ApiServer._bgtask_running: return { "status": "running", "running": True, "step": ApiServer._bt.get_action() if ApiServer._bt else str(BacktestState.STARTUP), "progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, + "trade_count": Trade.get_trades_proxy(is_open=False), "status_msg": "Backtest running", } From 03140a0ecbee240446bae65791a12f69feb2092a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Mar 2021 10:45:44 +0100 Subject: [PATCH 12/31] Run webserver in main thread when using webserver mode --- freqtrade/rpc/api_server/api_v1.py | 4 ++-- freqtrade/rpc/api_server/webserver.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 339f8565a..fd3cb345e 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -339,14 +339,14 @@ def api_get_backtest(): Get backtesting result. Returns Result after backtesting has been ran. """ - from freqtrade.persistence import Trade + from freqtrade.persistence import LocalTrade if ApiServer._bgtask_running: return { "status": "running", "running": True, "step": ApiServer._bt.get_action() if ApiServer._bt else str(BacktestState.STARTUP), "progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, - "trade_count": Trade.get_trades_proxy(is_open=False), + "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", } diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index eccbadcb3..ac394b59d 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -166,6 +166,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.") From 134c61126e8006004a6f48ea35281fc3aad2c1f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Mar 2021 15:56:36 +0100 Subject: [PATCH 13/31] Properly track bt progress ... --- freqtrade/enums/backteststate.py | 3 ++- freqtrade/optimize/backtesting.py | 34 +++++++++++------------------- freqtrade/optimize/bt_progress.py | 34 ++++++++++++++++++++++++++++++ freqtrade/rpc/api_server/api_v1.py | 10 ++++----- 4 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 freqtrade/optimize/bt_progress.py diff --git a/freqtrade/enums/backteststate.py b/freqtrade/enums/backteststate.py index 4c1bd7cbc..490814497 100644 --- a/freqtrade/enums/backteststate.py +++ b/freqtrade/enums/backteststate.py @@ -8,7 +8,8 @@ class BacktestState(Enum): STARTUP = 1 DATALOAD = 2 ANALYZE = 3 - BACKTEST = 4 + CONVERT = 4 + BACKTEST = 5 def __str__(self): return f"{self.name.lower()}" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9168a0ddb..c8d4efa74 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,6 +21,7 @@ from freqtrade.enums import BacktestState, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin +from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.persistence import LocalTrade, PairLocks, Trade @@ -118,26 +119,13 @@ class Backtesting: # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) - self._progress = 0 - self._max_steps = 0 - self._action = BacktestState.STARTUP + self.progress = BTProgress() def __del__(self): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True - def set_progress(self, action: BacktestState, progress: float): - self._progress = 0 - self._action = action - - def get_progress(self) -> float: - - return round(self._progress / self._max_steps, 5) if self._max_steps > 0 else 0 - - def get_action(self) -> str: - return str(self._action) - def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting @@ -160,7 +148,7 @@ class Backtesting: Loads backtest data and returns the data combined with the timerange as tuple. """ - self.set_progress(BacktestState.DATALOAD, 0) + self.progress.init_step(BacktestState.DATALOAD, 1) timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -185,7 +173,7 @@ class Backtesting: timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) - self.set_progress(BacktestState.DATALOAD, 1) + self.progress.set_new_value(1) return data, timerange def prepare_backtest(self, enable_protections): @@ -210,8 +198,11 @@ class Backtesting: # and eventually change the constants for indexes at the top headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] data: Dict = {} + self.progress.init_step(BacktestState.CONVERT, len(processed)) + # Create dict with data for pair, pair_data in processed.items(): + self.progress.increment() if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist @@ -422,8 +413,8 @@ class Backtesting: open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 - self.set_progress(BacktestState.BACKTEST, 0) - self._max_steps = int((end_date - start_date) / timedelta(minutes=self.timeframe_min)) + self.progress.init_step(BacktestState.BACKTEST, int( + (end_date - start_date) / timedelta(minutes=self.timeframe_min))) # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: @@ -484,7 +475,7 @@ class Backtesting: self.protections.global_stop(tmp) # Move time one configured time_interval ahead. - self._progress += 1 + self.progress.increment() tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) @@ -500,8 +491,8 @@ class Backtesting: } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): - self.set_progress(BacktestState.ANALYZE, 0) - self._max_steps = 0 + self.progress.init_step(BacktestState.ANALYZE, 0) + logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) @@ -528,7 +519,6 @@ class Backtesting: "No data left after adjusting for startup candles.") min_date, max_date = history.get_timerange(preprocessed) - self.set_progress(BacktestState.BACKTEST, 0) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/freqtrade/optimize/bt_progress.py b/freqtrade/optimize/bt_progress.py new file mode 100644 index 000000000..8d4fd1737 --- /dev/null +++ b/freqtrade/optimize/bt_progress.py @@ -0,0 +1,34 @@ + +from freqtrade.enums import BacktestState + + +class BTProgress: + _action: BacktestState = BacktestState.STARTUP + _progress: float = 0 + _max_steps: float = 0 + + def __init__(self): + pass + + def init_step(self, action: BacktestState, max_steps: float): + self._action = action + self._max_steps = max_steps + self._proress = 0 + + def set_new_value(self, new_value: float): + self._progress = new_value + + def increment(self): + self._progress += 1 + + @property + def progress(self): + """ + Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors). + """ + return max(min(round(self._progress / self._max_steps, 5) + if self._max_steps > 0 else 0, 1), 0) + + @property + def action(self): + return str(self._action) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index fd3cb345e..0e3b16baf 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -291,6 +291,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac if (not ApiServer._bt or lastconfig.get('timeframe') != strat.timeframe + 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('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): @@ -298,11 +299,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) # Reset data if backtesting is reloaded - # TODO: is this always necessary?? - ApiServer._backtestdata = None if (not ApiServer._backtestdata or not ApiServer._bt_timerange - or lastconfig.get('timerange') != btconfig['timerange']): + or lastconfig.get('timerange') != btconfig['timerange'] + or lastconfig.get('timeframe') != strat.timeframe): lastconfig['timerange'] = btconfig['timerange'] lastconfig['protections'] = btconfig.get('protections', []) lastconfig['enable_protections'] = btconfig.get('enable_protections') @@ -344,8 +344,8 @@ def api_get_backtest(): return { "status": "running", "running": True, - "step": ApiServer._bt.get_action() if ApiServer._bt else str(BacktestState.STARTUP), - "progress": ApiServer._bt.get_progress() if ApiServer._bt else 0, + "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", } From 85663060107e8e13b4bedf74e2db17a3fbbbcbd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 07:49:54 +0200 Subject: [PATCH 14/31] Add test for start_websever --- freqtrade/commands/trade_commands.py | 3 ++- tests/commands/test_commands.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index ca1451778..3c3f3db9f 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -1,6 +1,7 @@ import logging from typing import Any, Dict + logger = logging.getLogger(__name__) @@ -32,9 +33,9 @@ def start_webserver(args: Dict[str, Any]) -> None: """ Main entry point for webserver mode """ - from freqtrade.rpc.api_server import ApiServer from freqtrade.configuration import Configuration from freqtrade.enums import RunMode + from freqtrade.rpc.api_server import ApiServer # Initialize configuration config = Configuration(args, RunMode.WEBSERVER).get_config() diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index dcceb3ea1..bef421c89 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -16,6 +16,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_test_pairlist, start_trading) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) +from freqtrade.commands.trade_commands import start_webserver from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException @@ -58,6 +59,18 @@ def test_start_trading_fail(mocker, caplog): assert log_has('Fatal exception!', caplog) +def test_start_webserver(mocker, caplog): + + api_server_mock = mocker.patch("freqtrade.rpc.api_server.ApiServer", ) + + args = [ + 'webserver', + '-c', 'config_bittrex.json.example' + ] + start_webserver(get_args(args)) + assert api_server_mock.call_count == 1 + + def test_list_exchanges(capsys): args = [ From 804d99cce91628075de9c7fd317f9bca8270cccf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 07:55:29 +0200 Subject: [PATCH 15/31] Move backtesting api to it's own file --- freqtrade/rpc/api_server/api_backtest.py | 149 +++++++++++++++++++++++ freqtrade/rpc/api_server/api_v1.py | 134 +------------------- freqtrade/rpc/api_server/webserver.py | 4 + 3 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 freqtrade/rpc/api_server/api_backtest.py diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py new file mode 100644 index 000000000..ea566f1f1 --- /dev/null +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -0,0 +1,149 @@ + +import asyncio +import logging +from copy import deepcopy + +from fastapi import APIRouter, BackgroundTasks, Depends + +from freqtrade.enums import BacktestState +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._lastbacktestconfig + strat = StrategyResolver.load_strategy(btconfig) + + if (not ApiServer._bt + or lastconfig.get('timeframe') != strat.timeframe + 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('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): + # TODO: Investigate if enabling protections can be dynamically ingested from here... + from freqtrade.optimize.backtesting import Backtesting + ApiServer._bt = Backtesting(btconfig) + # Reset data if backtesting is reloaded + + if (not ApiServer._backtestdata or not ApiServer._bt_timerange + or lastconfig.get('timerange') != btconfig['timerange'] + 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._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() + + min_date, max_date = ApiServer._bt.backtest_one_strategy( + strat, ApiServer._backtestdata, + ApiServer._bt_timerange) + ApiServer._bt.results = generate_backtest_stats( + ApiServer._backtestdata, ApiServer._bt.all_results, + min_date=min_date, max_date=max_date) + logger.info("Backtesting finished.") + + 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": "Backtesting 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._backtestdata + ApiServer._backtestdata = None + logger.info("Backtesting reset") + return { + "status": "reset", + "running": False, + "step": "", + "progress": 0, + "status_msg": "Backtesting reset", + } diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 0e3b16baf..3f3f799b0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -1,10 +1,9 @@ -import asyncio import logging from copy import deepcopy from pathlib import Path from typing import List, Optional -from fastapi import APIRouter, BackgroundTasks, Depends +from fastapi import APIRouter, Depends from fastapi.exceptions import HTTPException from freqtrade import __version__ @@ -12,7 +11,7 @@ from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC -from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestRequest, BacktestResponse, +from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceBuyPayload, ForceBuyResponse, @@ -23,7 +22,6 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, BacktestReques WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException -from freqtrade.state import BacktestState logger = logging.getLogger(__name__) @@ -263,131 +261,3 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option } return result - -@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._lastbacktestconfig - strat = StrategyResolver.load_strategy(btconfig) - - if (not ApiServer._bt - or lastconfig.get('timeframe') != strat.timeframe - 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('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): - # TODO: Investigate if enabling protections can be dynamically ingested from here... - from freqtrade.optimize.backtesting import Backtesting - ApiServer._bt = Backtesting(btconfig) - # Reset data if backtesting is reloaded - - if (not ApiServer._backtestdata or not ApiServer._bt_timerange - or lastconfig.get('timerange') != btconfig['timerange'] - 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._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() - - min_date, max_date = ApiServer._bt.backtest_one_strategy( - strat, ApiServer._backtestdata, - ApiServer._bt_timerange) - ApiServer._bt.results = generate_backtest_stats( - ApiServer._backtestdata, ApiServer._bt.all_results, - min_date=min_date, max_date=max_date) - logger.info("Backtesting finished.") - - 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": "Backtesting 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, - "progress": 0, - "status_msg": "Backtest running", - } - if ApiServer._bt: - del ApiServer._bt - ApiServer._bt = None - del ApiServer._backtestdata - ApiServer._backtestdata = None - logger.info("Backtesting reset") - return { - "status": "reset", - "running": False, - "progress": 0, - "status_msg": "Backtesting reset", - } diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index ac394b59d..0b58d79ff 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -112,12 +112,16 @@ class ApiServer(RPCHandler): 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 + from freqtrade.rpc.api_server.api_backtest import router as api_backtest app.include_router(api_v1_public, prefix="/api/v1") 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='') From b44d215b90a5253d1069a43f6e9b7f6a751108c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 19:59:49 +0200 Subject: [PATCH 16/31] Add test for backtest via APII --- freqtrade/rpc/api_server/api_backtest.py | 6 +-- tests/rpc/test_rpc_apiserver.py | 66 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index ea566f1f1..48aee0cc5 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -70,7 +70,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac ApiServer._bt.results = generate_backtest_stats( ApiServer._backtestdata, ApiServer._bt.all_results, min_date=min_date, max_date=max_date) - logger.info("Backtesting finished.") + logger.info("Backtest finished.") finally: ApiServer._bgtask_running = False @@ -110,7 +110,7 @@ def api_get_backtest(): "running": False, "step": "", "progress": 0, - "status_msg": "Backtesting not yet executed" + "status_msg": "Backtest not yet executed" } return { @@ -145,5 +145,5 @@ def api_delete_backtest(): "running": False, "step": "", "progress": 0, - "status_msg": "Backtesting reset", + "status_msg": "Backtest reset", } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a8ffd3eff..329d5e32f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,6 +2,7 @@ Unit test file for rpc/api_server.py """ +import json from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock @@ -1227,3 +1228,68 @@ def test_list_available_pairs(botclient): assert rc.json()['length'] == 1 assert rc.json()['pairs'] == ['XRP/ETH'] assert len(rc.json()['pair_interval']) == 1 + + +def test_api_backtesting(botclient, mocker, fee): + ftbot, client = botclient + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + + # Backtesting not started yet + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + + result = rc.json() + assert result['status'] == 'not_started' + assert not result['running'] + assert result['status_msg'] == 'Backtest not yet executed' + assert result['progress'] == 0 + + # Reset backtesting + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' + + # bt_mock = mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', + # return_value=(1, 2)) + # stats_mock = mocker.patch('freqtrade.optimize.optimize_reports.generate_backtest_stats') + # bt_mock.load_bt_data = MagicMock(return_value=(xxx, 'asdfadf')) + # start backtesting + data = { + "strategy": "DefaultStrategy", + "timeframe": "5m", + "timerange": "20180110-20180111", + "max_open_trades": 3, + "stake_amount": 100, + "dry_run_wallet": 1000, + "enable_protections": False + } + rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data)) + assert_response(rc) + result = rc.json() + + assert result['status'] == 'running' + assert result['progress'] == 0 + assert result['running'] + assert result['status_msg'] == 'Backtest started' + + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + + result = rc.json() + assert result['status'] == 'ended' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' + assert result['progress'] == 1 + assert result['backtest_result'] + + # Delete backtesting to avoid leakage since the backtest-object may stick around. + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' From 17b3cc20976a4b63e622d64f82808c28db23498a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Apr 2021 19:58:08 +0200 Subject: [PATCH 17/31] Return numeric value, not empty string --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 538e95f40..c81f57e90 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -118,9 +118,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), From 129c7b02d021f91d1ff810038639df955c155680 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Apr 2021 20:00:14 +0200 Subject: [PATCH 18/31] Not all config values are mandatory in webserver mode --- freqtrade/rpc/api_server/api_schemas.py | 8 ++++---- freqtrade/rpc/api_server/api_v1.py | 18 ++++++++---------- freqtrade/rpc/api_server/webserver.py | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index f10c501f7..12ca87edb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -118,17 +118,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] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3f3f799b0..f875a9e3c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,15 +11,14 @@ from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC -from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, - Balances, BlacklistPayload, BlacklistResponse, - Count, Daily, DeleteLockRequest, DeleteTrade, - ForceBuyPayload, ForceBuyResponse, - ForceSellPayload, Locks, Logs, OpenTradeSchema, - PairHistory, PerformanceEntry, Ping, PlotConfig, - Profit, ResultMsg, ShowConfig, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, Version, - WhitelistResponse) +from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, + BlacklistResponse, Count, Daily, + DeleteLockRequest, DeleteTrade, ForceBuyPayload, + ForceBuyResponse, ForceSellPayload, Locks, Logs, + OpenTradeSchema, PairHistory, PerformanceEntry, + Ping, PlotConfig, Profit, ResultMsg, ShowConfig, + Stats, StatusMsg, StrategyListResponse, + StrategyResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -260,4 +259,3 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option 'pair_interval': pair_interval, } return result - diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 0b58d79ff..5a59883ea 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -109,10 +109,10 @@ 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 - from freqtrade.rpc.api_server.api_backtest import router as api_backtest app.include_router(api_v1_public, prefix="/api/v1") From 830b2548bcb2498d635ece241d135aef0e33b03c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 19:58:53 +0200 Subject: [PATCH 19/31] Add backtest stopping --- freqtrade/optimize/backtesting.py | 6 +++++- freqtrade/rpc/api_server/api_backtest.py | 25 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c8d4efa74..a1cf94774 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -120,6 +120,7 @@ class Backtesting: self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) self.progress = BTProgress() + self.abort = False def __del__(self): LoggingMixin.show_output = True @@ -202,6 +203,8 @@ class Backtesting: # Create dict with data for pair, pair_data in processed.items(): + if self.abort: + raise DependencyException("Stop requested") self.progress.increment() if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist @@ -419,7 +422,8 @@ class Backtesting: # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: open_trade_count_start = open_trade_count - + if self.abort: + raise DependencyException("Stop requested") for i, pair in enumerate(data): row_index = indexes[pair] try: diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 48aee0cc5..bd202ea64 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -6,6 +6,7 @@ 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 @@ -64,6 +65,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac lastconfig['timeframe'] = strat.timeframe ApiServer._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() + ApiServer._bt.abort = False min_date, max_date = ApiServer._bt.backtest_one_strategy( strat, ApiServer._backtestdata, ApiServer._bt_timerange) @@ -72,6 +74,9 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac 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 @@ -147,3 +152,23 @@ def api_delete_backtest(): "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", + } From 2ec22f1d97090b10506c298f9a119dd4eb14e6f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 May 2021 07:47:38 +0200 Subject: [PATCH 20/31] Add Sorting to available pair list --- freqtrade/rpc/api_server/api_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f875a9e3c..61d69707e 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -252,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, From e5b1657ab30f5b8b05840b74de8c8995ee3982a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 12:54:05 +0200 Subject: [PATCH 21/31] Properly remove rpc handler --- freqtrade/rpc/api_server/webserver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 5a59883ea..e9becc936 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -76,7 +76,6 @@ class ApiServer(RPCHandler): Attach rpc handler """ if not self._has_rpc: - self._rpc = rpc ApiServer._rpc = rpc ApiServer._has_rpc = True else: @@ -85,7 +84,9 @@ class ApiServer(RPCHandler): 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() From 5474d5ee64b65a178a195e97ca16b3e797e34f33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 15:18:41 +0200 Subject: [PATCH 22/31] Move webserver start command to seperate file --- freqtrade/commands/__init__.py | 3 ++- freqtrade/commands/trade_commands.py | 13 ------------- freqtrade/commands/webserver_commands.py | 15 +++++++++++++++ freqtrade/rpc/api_server/api_backtest.py | 1 - tests/commands/test_commands.py | 3 +-- 5 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 freqtrade/commands/webserver_commands.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index a13d0ba49..04e46ee23 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -19,4 +19,5 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_h from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit -from freqtrade.commands.trade_commands import start_trading, start_webserver +from freqtrade.commands.trade_commands import start_trading +from freqtrade.commands.webserver_commands import start_webserver diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 3c3f3db9f..535844844 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -27,16 +27,3 @@ def start_trading(args: Dict[str, Any]) -> int: logger.info("worker found ... calling exit") worker.exit() return 0 - - -def start_webserver(args: Dict[str, Any]) -> None: - """ - Main entry point for webserver mode - """ - from freqtrade.configuration import Configuration - from freqtrade.enums import RunMode - from freqtrade.rpc.api_server import ApiServer - - # Initialize configuration - config = Configuration(args, RunMode.WEBSERVER).get_config() - ApiServer(config, standalone=True) diff --git a/freqtrade/commands/webserver_commands.py b/freqtrade/commands/webserver_commands.py new file mode 100644 index 000000000..9a5975227 --- /dev/null +++ b/freqtrade/commands/webserver_commands.py @@ -0,0 +1,15 @@ +from typing import Any, Dict + +from freqtrade.enums import RunMode + + +def start_webserver(args: Dict[str, Any]) -> None: + """ + Main entry point for webserver mode + """ + from freqtrade.configuration import Configuration + from freqtrade.rpc.api_server import ApiServer + + # Initialize configuration + config = Configuration(args, RunMode.WEBSERVER).get_config() + ApiServer(config, standalone=True) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index bd202ea64..bdd9bc522 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -1,4 +1,3 @@ - import asyncio import logging from copy import deepcopy diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index bef421c89..dfbbbb2a6 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -13,10 +13,9 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, - start_test_pairlist, start_trading) + start_test_pairlist, start_trading, start_webserver) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) -from freqtrade.commands.trade_commands import start_webserver from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException From 005da9718346e6b870210acf3b8b4d0582c06883 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jul 2021 06:28:47 +0200 Subject: [PATCH 23/31] extract backtesting abort functionality --- freqtrade/optimize/backtesting.py | 15 +++++++++++---- freqtrade/rpc/api_server/api_backtest.py | 2 +- tests/optimize/test_backtesting.py | 14 ++++++++++++++ tests/rpc/test_rpc_apiserver.py | 11 +++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a1cf94774..e507ab2fd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -189,6 +189,15 @@ class Backtesting: self.rejected_trades = 0 self.dataprovider.clear_cache() + def check_abort(self): + """ + Check if abort was requested, raise DependencyException if that's the case + Only applies to Interactive backtest mode (webserver mode) + """ + if self.abort: + self.abort = False + raise DependencyException("Stop requested") + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -203,8 +212,7 @@ class Backtesting: # Create dict with data for pair, pair_data in processed.items(): - if self.abort: - raise DependencyException("Stop requested") + self.check_abort() self.progress.increment() if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist @@ -422,8 +430,7 @@ class Backtesting: # Loop timerange and get candle for each pair at that point in time while tmp <= end_date: open_trade_count_start = open_trade_count - if self.abort: - raise DependencyException("Stop requested") + self.check_abort() for i, pair in enumerate(data): row_index = indexes[pair] try: diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index bdd9bc522..cd40438d2 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -52,8 +52,8 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac # TODO: Investigate if enabling protections can be dynamically ingested from here... from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) - # Reset data if backtesting is reloaded + # Only reload data if timeframe or timerange changed. if (not ApiServer._backtestdata or not ApiServer._bt_timerange or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timeframe') != strat.timeframe): diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 30d86f979..5427bbf92 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -346,6 +346,20 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC']) +def test_backtest_abort(default_conf, mocker, testdatadir) -> None: + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + backtesting.check_abort() + + backtesting.abort = True + + with pytest.raises(DependencyException, match="Stop requested"): + backtesting.check_abort() + # abort flag resets + assert backtesting.abort is False + assert backtesting.progress.progress == 0 + + def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 329d5e32f..c3a958ff5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1252,10 +1252,6 @@ def test_api_backtesting(botclient, mocker, fee): assert not result['running'] assert result['status_msg'] == 'Backtest reset' - # bt_mock = mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', - # return_value=(1, 2)) - # stats_mock = mocker.patch('freqtrade.optimize.optimize_reports.generate_backtest_stats') - # bt_mock.load_bt_data = MagicMock(return_value=(xxx, 'asdfadf')) # start backtesting data = { "strategy": "DefaultStrategy", @@ -1285,6 +1281,13 @@ def test_api_backtesting(botclient, mocker, fee): assert result['progress'] == 1 assert result['backtest_result'] + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'not_running' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' + # Delete backtesting to avoid leakage since the backtest-object may stick around. rc = client_delete(client, f"{BASE_URI}/backtest") assert_response(rc) From 36d4a15d248ebc655803a828c64824e84fb23bb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jul 2021 06:46:58 +0200 Subject: [PATCH 24/31] quickly document webserver mode --- docs/utils.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/utils.md b/docs/utils.md index 524fefc21..789462de4 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -614,6 +614,48 @@ Show whitelist when using a [dynamic pairlist](plugins.md#pairlists). freqtrade test-pairlist --config config.json --quote USDT BTC ``` +## Webserver mode + +!!! Warning "Experimental" + Webserver mode is an experimental mode to increase backesting and strategy development productivity. + There may still be bugs - so if you happen to stumble across these, please report them as github issues, thanks. + +Run freqtrade in webserver mode. +Freqtrade will start the webserver and allow FreqUI to start and control backtesting processes. +This has the advantage that data will not be reloaded between backtesting runs (as long as timeframe and timerange remain identical). +FreqUI will also show the backtesting results. + +``` +usage: freqtrade webserver [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + +optional arguments: + -h, --help show this help message and exit + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + ## List Hyperopt results You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command. From a4bd862323deb86febe3bfa39af124642df909df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jul 2021 20:29:04 +0200 Subject: [PATCH 25/31] Fix fluky test --- tests/rpc/test_rpc_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 918022386..596b5ae20 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPCManager +from freqtrade.rpc.api_server.webserver import ApiServer from tests.conftest import get_patched_freqtradebot, log_has @@ -190,3 +191,4 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: assert len(rpc_manager.registered_modules) == 1 assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] assert run_mock.call_count == 1 + ApiServer.shutdown() From b7a9853d9ab245d9158536bb5110c72e96c09fe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jul 2021 21:04:52 +0200 Subject: [PATCH 26/31] Increase test coverage --- tests/rpc/test_rpc_apiserver.py | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c3a958ff5..f145db3e0 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -17,7 +17,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.enums import RunMode, State -from freqtrade.exceptions import ExchangeError +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC @@ -243,6 +243,9 @@ def test_api__init__(default_conf, mocker): apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) assert apiserver._config == default_conf + with pytest.raises(OperationalException, match="RPC Handler already attached."): + apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) + ApiServer.shutdown() @@ -1288,6 +1291,38 @@ def test_api_backtesting(botclient, mocker, fee): assert not result['running'] assert result['status_msg'] == 'Backtest ended' + # Simulate running backtest + ApiServer._bgtask_running = True + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'stopping' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' + + # Get running backtest... + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' + assert result['running'] + assert result['step'] == "backtest" + assert result['status_msg'] == "Backtest running" + + # Try delete with task still running + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' + + # Post to backtest that's still running + rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data)) + assert_response(rc, 502) + result = rc.json() + assert 'Bot Background task already running' in result['error'] + + ApiServer._bgtask_running = False + # Delete backtesting to avoid leakage since the backtest-object may stick around. rc = client_delete(client, f"{BASE_URI}/backtest") assert_response(rc) From 8be0241573f0cf1bf1bac92b5e92d4c85ab44a76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Jul 2021 07:07:56 +0200 Subject: [PATCH 27/31] Build images for feat/** branches --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42959c3b5..2606cac4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - stable - develop + - feat/** tags: release: types: [published] From cf6f706078fc5ddb75901d76e6ae8b87fd32330f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Jul 2021 10:01:15 +0200 Subject: [PATCH 28/31] Don't build for feat branches that breaks CI for PR's --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2606cac4d..42959c3b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: - master - stable - develop - - feat/** tags: release: types: [published] From 72a103f32d3c9c70c54eea9849902f6a9c6641ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Jul 2021 10:13:23 +0200 Subject: [PATCH 29/31] Properly test webserver startup in standalone mode --- tests/rpc/test_rpc_apiserver.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f145db3e0..ad8f2c972 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -17,7 +17,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.enums import RunMode, State -from freqtrade.exceptions import ExchangeError, OperationalException +from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC @@ -308,7 +308,10 @@ def test_api_run(default_conf, mocker, caplog): }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - server_mock = MagicMock() + server_inst_mock = MagicMock() + server_inst_mock.run_in_thread = MagicMock() + server_inst_mock.run = MagicMock() + server_mock = MagicMock(return_value=server_inst_mock) mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock) apiserver = ApiServer(default_conf) @@ -318,6 +321,8 @@ def test_api_run(default_conf, mocker, caplog): assert apiserver._config == default_conf apiserver.start_api() assert server_mock.call_count == 2 + assert server_inst_mock.run_in_thread.call_count == 2 + assert server_inst_mock.run.call_count == 0 assert server_mock.call_args_list[0][0][0].host == "127.0.0.1" assert server_mock.call_args_list[0][0][0].port == 8080 assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) @@ -336,6 +341,8 @@ def test_api_run(default_conf, mocker, caplog): apiserver.start_api() assert server_mock.call_count == 1 + assert server_inst_mock.run_in_thread.call_count == 1 + assert server_inst_mock.run.call_count == 0 assert server_mock.call_args_list[0][0][0].host == "0.0.0.0" assert server_mock.call_args_list[0][0][0].port == 8089 assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) @@ -349,6 +356,17 @@ def test_api_run(default_conf, mocker, caplog): "Please make sure that this is intentional!", caplog) assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog) + server_mock.reset_mock() + apiserver._standalone = True + apiserver.start_api() + assert server_inst_mock.run_in_thread.call_count == 0 + assert server_inst_mock.run.call_count == 1 + + apiserver1 = ApiServer(default_conf) + assert id(apiserver1) == id(apiserver) + + apiserver._standalone = False + # Test crashing API server caplog.clear() mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', @@ -1233,7 +1251,7 @@ def test_list_available_pairs(botclient): assert len(rc.json()['pair_interval']) == 1 -def test_api_backtesting(botclient, mocker, fee): +def test_api_backtesting(botclient, mocker, fee, caplog): ftbot, client = botclient mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) @@ -1323,6 +1341,11 @@ def test_api_backtesting(botclient, mocker, fee): ApiServer._bgtask_running = False + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', + side_effect=DependencyException()) + rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data)) + assert log_has("Backtesting caused an error: ", caplog) + # Delete backtesting to avoid leakage since the backtest-object may stick around. rc = client_delete(client, f"{BASE_URI}/backtest") assert_response(rc) From ad26b0dad057eaa95d798ba4f06ba627452e91f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Jul 2021 10:59:00 +0200 Subject: [PATCH 30/31] Don't void backtest object when not necessary --- freqtrade/optimize/bt_progress.py | 1 - freqtrade/rpc/api_server/api_backtest.py | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/bt_progress.py b/freqtrade/optimize/bt_progress.py index 8d4fd1737..d295956c7 100644 --- a/freqtrade/optimize/bt_progress.py +++ b/freqtrade/optimize/bt_progress.py @@ -1,4 +1,3 @@ - from freqtrade.enums import BacktestState diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index cd40438d2..8bf87a044 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -45,17 +45,16 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac if (not ApiServer._bt or lastconfig.get('timeframe') != strat.timeframe - 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('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)): - # TODO: Investigate if enabling protections can be dynamically ingested from here... from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) # Only reload data if timeframe or timerange changed. if (not ApiServer._backtestdata 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', []) From 52ae95b2a5b0625c71e1ce067a27c415320d1e37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Jul 2021 11:19:23 +0200 Subject: [PATCH 31/31] Improve naming of apiserver variables --- freqtrade/commands/arguments.py | 2 +- freqtrade/rpc/api_server/api_backtest.py | 36 +++++++++++++----------- freqtrade/rpc/api_server/webserver.py | 4 +-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 1efe450ff..1143db394 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -390,6 +390,6 @@ class Arguments: # Add webserver subcommand webserver_cmd = subparsers.add_parser('webserver', help='Webserver module.', - parents=[_common_parser, _strategy_parser]) + parents=[_common_parser]) webserver_cmd.set_defaults(func=start_webserver) self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 8bf87a044..76b4a8169 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -40,35 +40,39 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy - lastconfig = ApiServer._lastbacktestconfig + 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)): + 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._backtestdata 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): + 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._backtestdata, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() + 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._backtestdata, - ApiServer._bt_timerange) + strat, ApiServer._bt_data, ApiServer._bt_timerange) ApiServer._bt.results = generate_backtest_stats( - ApiServer._backtestdata, ApiServer._bt.all_results, + ApiServer._bt_data, ApiServer._bt.all_results, min_date=min_date, max_date=max_date) logger.info("Backtest finished.") @@ -140,8 +144,8 @@ def api_delete_backtest(): if ApiServer._bt: del ApiServer._bt ApiServer._bt = None - del ApiServer._backtestdata - ApiServer._backtestdata = None + del ApiServer._bt_data + ApiServer._bt_data = None logger.info("Backtesting reset") return { "status": "reset", diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index e9becc936..235063191 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -35,9 +35,9 @@ class ApiServer(RPCHandler): _rpc: RPC # Backtesting type: Backtesting _bt = None - _backtestdata = None + _bt_data = None _bt_timerange = None - _lastbacktestconfig: Dict[str, Any] = {} + _bt_last_config: Dict[str, Any] = {} _has_rpc: bool = False _bgtask_running: bool = False _config: Dict[str, Any] = {}