Merge pull request #5243 from freqtrade/feat/webservermode_progress

Introduce webserver mode subcommand
This commit is contained in:
Matthias 2021-07-18 10:48:55 +02:00 committed by GitHub
commit 7b7d9c02d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 595 additions and 34 deletions

View File

@ -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.

View File

@ -20,3 +20,4 @@ from freqtrade.commands.optimize_commands import start_backtesting, start_edge,
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.webserver_commands import start_webserver

View File

@ -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])
webserver_cmd.set_defaults(func=start_webserver)
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)

View File

@ -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)

View File

@ -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'] = {}

View File

@ -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

View File

@ -0,0 +1,15 @@
from enum import Enum
class BacktestState(Enum):
"""
Bot application states
"""
STARTUP = 1
DATALOAD = 2
ANALYZE = 3
CONVERT = 4
BACKTEST = 5
def __str__(self):
return f"{self.name.lower()}"

View File

@ -14,6 +14,7 @@ class RunMode(Enum):
UTIL_EXCHANGE = "util_exchange"
UTIL_NO_EXCHANGE = "util_no_exchange"
PLOT = "plot"
WEBSERVER = "webserver"
OTHER = "other"

View File

@ -17,10 +17,11 @@ 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
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
@ -57,6 +58,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)
@ -118,6 +120,9 @@ class Backtesting:
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
self.progress = BTProgress()
self.abort = False
def __del__(self):
LoggingMixin.show_output = True
PairLocks.use_db = True
@ -147,6 +152,8 @@ class Backtesting:
Loads backtest data and returns the data combined with the timerange
as tuple.
"""
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')))
@ -170,6 +177,7 @@ class Backtesting:
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
self.required_startup, min_date)
self.progress.set_new_value(1)
return data, timerange
def prepare_backtest(self, enable_protections):
@ -184,6 +192,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.
@ -194,8 +211,12 @@ 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.check_abort()
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
@ -417,10 +438,13 @@ class Backtesting:
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0
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:
open_trade_count_start = open_trade_count
self.check_abort()
for i, pair in enumerate(data):
row_index = indexes[pair]
try:
@ -476,6 +500,7 @@ class Backtesting:
self.protections.global_stop(tmp)
# Move time one configured time_interval ahead.
self.progress.increment()
tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data)
@ -491,6 +516,8 @@ class Backtesting:
}
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
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)
@ -517,6 +544,7 @@ class Backtesting:
"No data left after adjusting for startup candles.")
min_date, max_date = history.get_timerange(preprocessed)
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).')
@ -551,11 +579,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)

View File

@ -0,0 +1,33 @@
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)

View File

@ -0,0 +1,176 @@
import asyncio
import logging
from copy import deepcopy
from fastapi import APIRouter, BackgroundTasks, Depends
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.deps import get_config
from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException
logger = logging.getLogger(__name__)
# Private API, protected by authentication
router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)):
"""Start backtesting if not done so already"""
if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running')
btconfig = deepcopy(config)
settings = dict(bt_settings)
# Pydantic models will contain all keys, but non-provided ones are None
for setting in settings.keys():
if settings[setting] is not None:
btconfig[setting] = settings[setting]
# Start backtesting
# Initialize backtesting object
def run_backtest():
from freqtrade.optimize.optimize_reports import generate_backtest_stats
from freqtrade.resolvers import StrategyResolver
asyncio.set_event_loop(asyncio.new_event_loop())
try:
# Reload strategy
lastconfig = ApiServer._bt_last_config
strat = StrategyResolver.load_strategy(btconfig)
if (
not ApiServer._bt
or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
):
from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig)
# Only reload data if timeframe or timerange changed.
if (
not ApiServer._bt_data
or not ApiServer._bt_timerange
or lastconfig.get('timerange') != btconfig['timerange']
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
or lastconfig.get('protections') != btconfig.get('protections', [])
or lastconfig.get('timeframe') != strat.timeframe
):
lastconfig['timerange'] = btconfig['timerange']
lastconfig['protections'] = btconfig.get('protections', [])
lastconfig['enable_protections'] = btconfig.get('enable_protections')
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
lastconfig['timeframe'] = strat.timeframe
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
ApiServer._bt.abort = False
min_date, max_date = ApiServer._bt.backtest_one_strategy(
strat, ApiServer._bt_data, ApiServer._bt_timerange)
ApiServer._bt.results = generate_backtest_stats(
ApiServer._bt_data, ApiServer._bt.all_results,
min_date=min_date, max_date=max_date)
logger.info("Backtest finished.")
except DependencyException as e:
logger.info(f"Backtesting caused an error: {e}")
pass
finally:
ApiServer._bgtask_running = False
background_tasks.add_task(run_backtest)
ApiServer._bgtask_running = True
return {
"status": "running",
"running": True,
"progress": 0,
"step": str(BacktestState.STARTUP),
"status_msg": "Backtest started",
}
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest():
"""
Get backtesting result.
Returns Result after backtesting has been ran.
"""
from freqtrade.persistence import LocalTrade
if ApiServer._bgtask_running:
return {
"status": "running",
"running": True,
"step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP),
"progress": ApiServer._bt.progress.progress if ApiServer._bt else 0,
"trade_count": len(LocalTrade.trades),
"status_msg": "Backtest running",
}
if not ApiServer._bt:
return {
"status": "not_started",
"running": False,
"step": "",
"progress": 0,
"status_msg": "Backtest not yet executed"
}
return {
"status": "ended",
"running": False,
"status_msg": "Backtest ended",
"step": "finished",
"progress": 1,
"backtest_result": ApiServer._bt.results,
}
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest():
"""Reset backtesting"""
if ApiServer._bgtask_running:
return {
"status": "running",
"running": True,
"step": "",
"progress": 0,
"status_msg": "Backtest running",
}
if ApiServer._bt:
del ApiServer._bt
ApiServer._bt = None
del ApiServer._bt_data
ApiServer._bt_data = None
logger.info("Backtesting reset")
return {
"status": "reset",
"running": False,
"step": "",
"progress": 0,
"status_msg": "Backtest reset",
}
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort():
if not ApiServer._bgtask_running:
return {
"status": "not_running",
"running": False,
"step": "",
"progress": 0,
"status_msg": "Backtest ended",
}
ApiServer._bt.abort = True
return {
"status": "stopping",
"running": False,
"step": "",
"progress": 0,
"status_msg": "Backtest ended",
}

View File

@ -123,17 +123,17 @@ class ShowConfig(BaseModel):
stake_currency_decimals: int
max_open_trades: int
minimal_roi: Dict[str, Any]
stoploss: float
trailing_stop: bool
stoploss: Optional[float]
trailing_stop: Optional[bool]
trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool]
use_custom_stoploss: Optional[bool]
timeframe: str
timeframe: Optional[str]
timeframe_ms: int
timeframe_min: int
exchange: str
strategy: str
strategy: Optional[str]
forcebuy_enabled: bool
ask_strategy: Dict[str, Any]
bid_strategy: Dict[str, Any]
@ -318,3 +318,24 @@ class PairHistory(BaseModel):
json_encoders = {
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
}
class BacktestRequest(BaseModel):
strategy: str
timeframe: Optional[str]
timerange: Optional[str]
max_open_trades: Optional[int]
stake_amount: Optional[Union[float, str]]
enable_protections: bool
dry_run_wallet: Optional[float]
class BacktestResponse(BaseModel):
status: str
running: bool
status_msg: str
step: str
progress: float
trade_count: Optional[float]
# TODO: Properly type backtestresult...
backtest_result: Optional[Dict[str, Any]]

View File

@ -1,3 +1,4 @@
import logging
from copy import deepcopy
from pathlib import Path
from typing import List, Optional
@ -22,6 +23,8 @@ from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
logger = logging.getLogger(__name__)
# Public API, requires no auth.
router_public = APIRouter()
# Private API, protected by authentication
@ -249,7 +252,7 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})
pairs.sort()
result = {
'length': len(pairs),
'pairs': pairs,

View File

@ -8,6 +8,7 @@ from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from freqtrade.exceptions import OperationalException
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
@ -28,17 +29,37 @@ class FTJSONResponse(JSONResponse):
class ApiServer(RPCHandler):
__instance = None
__initialized = False
_rpc: RPC
# Backtesting type: Backtesting
_bt = None
_bt_data = None
_bt_timerange = None
_bt_last_config: Dict[str, Any] = {}
_has_rpc: bool = False
_bgtask_running: bool = False
_config: Dict[str, Any] = {}
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
super().__init__(rpc, config)
self._server = None
def __new__(cls, *args, **kwargs):
"""
This class is a singleton.
We'll only have one instance of it around.
"""
if ApiServer.__instance is None:
ApiServer.__instance = object.__new__(cls)
ApiServer.__initialized = False
return ApiServer.__instance
ApiServer._rpc = rpc
ApiServer._has_rpc = True
def __init__(self, config: Dict[str, Any], standalone: bool = False) -> None:
ApiServer._config = config
if self.__initialized and (standalone or self._standalone):
return
self._standalone: bool = standalone
self._server = None
ApiServer.__initialized = True
api_config = self._config['api_server']
self.app = FastAPI(title="Freqtrade API",
@ -50,12 +71,33 @@ class ApiServer(RPCHandler):
self.start_api()
def add_rpc_handler(self, rpc: RPC):
"""
Attach rpc handler
"""
if not self._has_rpc:
ApiServer._rpc = rpc
ApiServer._has_rpc = True
else:
# This should not happen assuming we didn't mess up.
raise OperationalException('RPC Handler already attached.')
def cleanup(self) -> None:
""" Cleanup pending module resources """
if self._server:
ApiServer._has_rpc = False
del ApiServer._rpc
if self._server and not self._standalone:
logger.info("Stopping API Server")
self._server.cleanup()
@classmethod
def shutdown(cls):
cls.__initialized = False
del cls.__instance
cls.__instance = None
cls._has_rpc = False
cls._rpc = None
def send_msg(self, msg: Dict[str, str]) -> None:
pass
@ -68,6 +110,7 @@ class ApiServer(RPCHandler):
def configure_app(self, app: FastAPI, config):
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
from freqtrade.rpc.api_server.web_ui import router_ui
@ -77,6 +120,9 @@ class ApiServer(RPCHandler):
app.include_router(api_v1, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
)
app.include_router(api_backtest, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
)
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
# UI Router MUST be last!
app.include_router(router_ui, prefix='')
@ -125,6 +171,9 @@ class ApiServer(RPCHandler):
)
try:
self._server = UvicornServer(uvconfig)
self._server.run_in_thread()
if self._standalone:
self._server.run()
else:
self._server.run_in_thread()
except Exception:
logger.exception("Api server failed to start.")

View File

@ -119,9 +119,9 @@ class RPC:
'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe']
) if 'timeframe' in config else '',
) if 'timeframe' in config else 0,
'timeframe_min': timeframe_to_minutes(config['timeframe']
) if 'timeframe' in config else '',
) if 'timeframe' in config else 0,
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),

View File

@ -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

View File

@ -13,7 +13,7 @@ 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.configuration import setup_utils_configuration
@ -58,6 +58,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 = [

View File

@ -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)

View File

@ -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
@ -16,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 DependencyException, ExchangeError, OperationalException
from freqtrade.loggers import setup_logging, setup_logging_pre
from freqtrade.persistence import PairLocks, Trade
from freqtrade.rpc import RPC
@ -48,9 +49,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 +240,13 @@ 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
with pytest.raises(OperationalException, match="RPC Handler already attached."):
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
ApiServer.shutdown()
def test_api_UvicornServer(mocker):
@ -298,15 +308,21 @@ 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(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
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)
@ -325,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)
@ -338,12 +356,24 @@ 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',
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 +389,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):
@ -1221,3 +1253,108 @@ 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, caplog):
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'
# 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']
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'
# 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
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)
result = rc.json()
assert result['status'] == 'reset'
assert not result['running']
assert result['status_msg'] == 'Backtest reset'

View File

@ -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()