2021-04-01 05:55:29 +00:00
|
|
|
import asyncio
|
|
|
|
import logging
|
|
|
|
from copy import deepcopy
|
2022-06-15 04:53:52 +00:00
|
|
|
from datetime import datetime
|
2023-07-25 18:34:45 +00:00
|
|
|
from pathlib import Path
|
2022-04-13 04:55:47 +00:00
|
|
|
from typing import Any, Dict, List
|
2021-04-01 05:55:29 +00:00
|
|
|
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
2022-09-27 18:37:16 +00:00
|
|
|
from fastapi.exceptions import HTTPException
|
2021-04-01 05:55:29 +00:00
|
|
|
|
2021-09-27 05:12:40 +00:00
|
|
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
2023-05-21 10:04:18 +00:00
|
|
|
from freqtrade.constants import Config
|
2023-07-31 19:16:25 +00:00
|
|
|
from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result,
|
|
|
|
get_backtest_resultlist, load_and_merge_backtest_result,
|
|
|
|
update_backtest_metadata)
|
2021-04-01 05:55:29 +00:00
|
|
|
from freqtrade.enums import BacktestState
|
2024-03-19 06:04:28 +00:00
|
|
|
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
|
2023-05-13 13:38:40 +00:00
|
|
|
from freqtrade.exchange.common import remove_exchange_credentials
|
2023-07-25 18:20:09 +00:00
|
|
|
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
|
2023-07-31 19:16:25 +00:00
|
|
|
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate,
|
|
|
|
BacktestRequest, BacktestResponse)
|
2023-06-01 05:07:02 +00:00
|
|
|
from freqtrade.rpc.api_server.deps import get_config
|
2023-05-21 07:08:52 +00:00
|
|
|
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
|
2021-04-01 05:55:29 +00:00
|
|
|
from freqtrade.rpc.rpc import RPCException
|
2023-07-30 08:52:23 +00:00
|
|
|
from freqtrade.types import get_BacktestResultType_default
|
2021-04-01 05:55:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2023-06-01 05:07:02 +00:00
|
|
|
# Private API, protected by authentication and webserver_mode dependency
|
2021-04-01 05:55:29 +00:00
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
2023-05-21 10:04:18 +00:00
|
|
|
def __run_backtest_bg(btconfig: Config):
|
|
|
|
from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats
|
|
|
|
from freqtrade.resolvers import StrategyResolver
|
|
|
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
|
|
try:
|
|
|
|
# Reload strategy
|
|
|
|
lastconfig = ApiBG.bt['last_config']
|
|
|
|
strat = StrategyResolver.load_strategy(btconfig)
|
|
|
|
validate_config_consistency(btconfig)
|
|
|
|
|
|
|
|
if (
|
|
|
|
not ApiBG.bt['bt']
|
|
|
|
or lastconfig.get('timeframe') != strat.timeframe
|
|
|
|
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
|
|
|
|
or lastconfig.get('timerange') != btconfig['timerange']
|
|
|
|
):
|
|
|
|
from freqtrade.optimize.backtesting import Backtesting
|
|
|
|
ApiBG.bt['bt'] = Backtesting(btconfig)
|
|
|
|
ApiBG.bt['bt'].load_bt_data_detail()
|
|
|
|
else:
|
|
|
|
ApiBG.bt['bt'].config = btconfig
|
|
|
|
ApiBG.bt['bt'].init_backtest()
|
|
|
|
# Only reload data if timeframe changed.
|
|
|
|
if (
|
|
|
|
not ApiBG.bt['data']
|
|
|
|
or not ApiBG.bt['timerange']
|
|
|
|
or lastconfig.get('timeframe') != strat.timeframe
|
|
|
|
or lastconfig.get('timerange') != btconfig['timerange']
|
|
|
|
):
|
|
|
|
ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[
|
|
|
|
'bt'].load_bt_data()
|
|
|
|
|
|
|
|
lastconfig['timerange'] = btconfig['timerange']
|
|
|
|
lastconfig['timeframe'] = strat.timeframe
|
|
|
|
lastconfig['protections'] = btconfig.get('protections', [])
|
|
|
|
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
|
|
|
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
|
|
|
|
|
|
|
ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False)
|
|
|
|
ApiBG.bt['bt'].strategylist = [strat]
|
2023-07-30 08:52:23 +00:00
|
|
|
ApiBG.bt['bt'].results = get_BacktestResultType_default()
|
2023-05-21 10:04:18 +00:00
|
|
|
ApiBG.bt['bt'].load_prior_backtest()
|
|
|
|
|
|
|
|
ApiBG.bt['bt'].abort = False
|
2023-08-03 05:05:57 +00:00
|
|
|
strategy_name = strat.get_strategy_name()
|
2023-05-21 10:04:18 +00:00
|
|
|
if (ApiBG.bt['bt'].results and
|
2023-08-03 05:05:57 +00:00
|
|
|
strategy_name in ApiBG.bt['bt'].results['strategy']):
|
2023-05-21 10:04:18 +00:00
|
|
|
# When previous result hash matches - reuse that result and skip backtesting.
|
2023-08-03 05:05:57 +00:00
|
|
|
logger.info(f'Reusing result of previous backtest for {strategy_name}')
|
2023-05-21 10:04:18 +00:00
|
|
|
else:
|
|
|
|
min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy(
|
|
|
|
strat, ApiBG.bt['data'], ApiBG.bt['timerange'])
|
|
|
|
|
|
|
|
ApiBG.bt['bt'].results = generate_backtest_stats(
|
|
|
|
ApiBG.bt['data'], ApiBG.bt['bt'].all_results,
|
|
|
|
min_date=min_date, max_date=max_date)
|
|
|
|
|
|
|
|
if btconfig.get('export', 'none') == 'trades':
|
2023-08-03 05:05:57 +00:00
|
|
|
fn = store_backtest_stats(
|
2023-05-21 10:04:18 +00:00
|
|
|
btconfig['exportfilename'], ApiBG.bt['bt'].results,
|
|
|
|
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
|
|
)
|
2023-08-03 05:05:57 +00:00
|
|
|
ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.name)
|
|
|
|
ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name
|
2023-05-21 10:04:18 +00:00
|
|
|
|
|
|
|
logger.info("Backtest finished.")
|
|
|
|
|
2024-03-19 06:04:28 +00:00
|
|
|
except ConfigurationError as e:
|
|
|
|
logger.error(f"Backtesting encountered a configuration Error: {e}")
|
|
|
|
|
2023-05-21 10:04:18 +00:00
|
|
|
except (Exception, OperationalException, DependencyException) as e:
|
|
|
|
logger.exception(f"Backtesting caused an error: {e}")
|
|
|
|
ApiBG.bt['bt_error'] = str(e)
|
|
|
|
finally:
|
|
|
|
ApiBG.bgtask_running = False
|
|
|
|
|
|
|
|
|
2021-04-01 05:55:29 +00:00
|
|
|
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
2023-05-21 10:04:18 +00:00
|
|
|
async def api_start_backtest(
|
2023-02-22 06:17:17 +00:00
|
|
|
bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
|
2023-06-01 05:07:02 +00:00
|
|
|
config=Depends(get_config)):
|
2023-05-21 07:12:02 +00:00
|
|
|
ApiBG.bt['bt_error'] = None
|
2021-04-01 05:55:29 +00:00
|
|
|
"""Start backtesting if not done so already"""
|
2023-05-21 07:12:02 +00:00
|
|
|
if ApiBG.bgtask_running:
|
2021-04-01 05:55:29 +00:00
|
|
|
raise RPCException('Bot Background task already running')
|
|
|
|
|
2022-09-27 18:37:16 +00:00
|
|
|
if ':' in bt_settings.strategy:
|
|
|
|
raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
|
|
|
|
|
2021-04-01 05:55:29 +00:00
|
|
|
btconfig = deepcopy(config)
|
2023-05-13 13:38:40 +00:00
|
|
|
remove_exchange_credentials(btconfig['exchange'], True)
|
2021-04-01 05:55:29 +00:00
|
|
|
settings = dict(bt_settings)
|
2022-12-21 05:28:55 +00:00
|
|
|
if settings.get('freqai', None) is not None:
|
2022-12-20 18:44:01 +00:00
|
|
|
settings['freqai'] = dict(settings['freqai'])
|
2021-04-01 05:55:29 +00:00
|
|
|
# Pydantic models will contain all keys, but non-provided ones are None
|
2022-12-20 18:32:29 +00:00
|
|
|
|
|
|
|
btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False)
|
2022-02-15 19:01:35 +00:00
|
|
|
try:
|
|
|
|
btconfig['stake_amount'] = float(btconfig['stake_amount'])
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2021-04-01 05:55:29 +00:00
|
|
|
|
2021-12-20 18:41:10 +00:00
|
|
|
# Force dry-run for backtesting
|
|
|
|
btconfig['dry_run'] = True
|
|
|
|
|
2021-04-01 05:55:29 +00:00
|
|
|
# Start backtesting
|
|
|
|
# Initialize backtesting object
|
2023-05-21 10:04:18 +00:00
|
|
|
|
|
|
|
background_tasks.add_task(__run_backtest_bg, btconfig=btconfig)
|
2023-05-21 07:12:02 +00:00
|
|
|
ApiBG.bgtask_running = True
|
2021-04-01 05:55:29 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
"status": "running",
|
|
|
|
"running": True,
|
|
|
|
"progress": 0,
|
|
|
|
"step": str(BacktestState.STARTUP),
|
|
|
|
"status_msg": "Backtest started",
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
2023-06-01 05:07:02 +00:00
|
|
|
def api_get_backtest():
|
2021-04-01 05:55:29 +00:00
|
|
|
"""
|
|
|
|
Get backtesting result.
|
|
|
|
Returns Result after backtesting has been ran.
|
|
|
|
"""
|
|
|
|
from freqtrade.persistence import LocalTrade
|
2023-05-21 07:12:02 +00:00
|
|
|
if ApiBG.bgtask_running:
|
2021-04-01 05:55:29 +00:00
|
|
|
return {
|
|
|
|
"status": "running",
|
|
|
|
"running": True,
|
2023-05-21 07:12:02 +00:00
|
|
|
"step": (ApiBG.bt['bt'].progress.action if ApiBG.bt['bt']
|
2023-02-22 06:17:17 +00:00
|
|
|
else str(BacktestState.STARTUP)),
|
2023-05-21 07:12:02 +00:00
|
|
|
"progress": ApiBG.bt['bt'].progress.progress if ApiBG.bt['bt'] else 0,
|
2021-04-01 05:55:29 +00:00
|
|
|
"trade_count": len(LocalTrade.trades),
|
|
|
|
"status_msg": "Backtest running",
|
|
|
|
}
|
|
|
|
|
2023-05-21 07:12:02 +00:00
|
|
|
if not ApiBG.bt['bt']:
|
2021-04-01 05:55:29 +00:00
|
|
|
return {
|
|
|
|
"status": "not_started",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
2021-04-01 17:59:49 +00:00
|
|
|
"status_msg": "Backtest not yet executed"
|
2021-04-01 05:55:29 +00:00
|
|
|
}
|
2023-05-21 07:12:02 +00:00
|
|
|
if ApiBG.bt['bt_error']:
|
2023-02-22 19:22:59 +00:00
|
|
|
return {
|
|
|
|
"status": "error",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
2023-05-21 07:12:02 +00:00
|
|
|
"status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}"
|
2023-02-22 19:22:59 +00:00
|
|
|
}
|
2021-04-01 05:55:29 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
"status": "ended",
|
|
|
|
"running": False,
|
|
|
|
"status_msg": "Backtest ended",
|
|
|
|
"step": "finished",
|
|
|
|
"progress": 1,
|
2023-05-21 07:12:02 +00:00
|
|
|
"backtest_result": ApiBG.bt['bt'].results,
|
2021-04-01 05:55:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
2023-06-01 05:07:02 +00:00
|
|
|
def api_delete_backtest():
|
2021-04-01 05:55:29 +00:00
|
|
|
"""Reset backtesting"""
|
2023-05-21 07:12:02 +00:00
|
|
|
if ApiBG.bgtask_running:
|
2021-04-01 05:55:29 +00:00
|
|
|
return {
|
|
|
|
"status": "running",
|
|
|
|
"running": True,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
|
|
|
"status_msg": "Backtest running",
|
|
|
|
}
|
2023-05-21 07:12:02 +00:00
|
|
|
if ApiBG.bt['bt']:
|
|
|
|
ApiBG.bt['bt'].cleanup()
|
|
|
|
del ApiBG.bt['bt']
|
|
|
|
ApiBG.bt['bt'] = None
|
|
|
|
del ApiBG.bt['data']
|
|
|
|
ApiBG.bt['data'] = None
|
2021-04-01 05:55:29 +00:00
|
|
|
logger.info("Backtesting reset")
|
|
|
|
return {
|
|
|
|
"status": "reset",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
2021-04-01 17:59:49 +00:00
|
|
|
"status_msg": "Backtest reset",
|
2021-04-01 05:55:29 +00:00
|
|
|
}
|
2021-04-05 17:58:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
2023-06-01 05:07:02 +00:00
|
|
|
def api_backtest_abort():
|
2023-05-21 07:12:02 +00:00
|
|
|
if not ApiBG.bgtask_running:
|
2021-04-05 17:58:53 +00:00
|
|
|
return {
|
|
|
|
"status": "not_running",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
|
|
|
"status_msg": "Backtest ended",
|
|
|
|
}
|
2023-05-21 07:12:02 +00:00
|
|
|
ApiBG.bt['bt'].abort = True
|
2021-04-05 17:58:53 +00:00
|
|
|
return {
|
|
|
|
"status": "stopping",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 0,
|
|
|
|
"status_msg": "Backtest ended",
|
|
|
|
}
|
2022-04-11 17:44:47 +00:00
|
|
|
|
|
|
|
|
2023-02-22 06:17:17 +00:00
|
|
|
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry],
|
|
|
|
tags=['webserver', 'backtest'])
|
2023-06-01 05:07:02 +00:00
|
|
|
def api_backtest_history(config=Depends(get_config)):
|
2022-04-11 17:44:47 +00:00
|
|
|
# Get backtest result history, read from metadata files
|
|
|
|
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
|
2022-04-11 18:04:47 +00:00
|
|
|
|
|
|
|
|
2023-02-22 06:17:17 +00:00
|
|
|
@router.get('/backtest/history/result', response_model=BacktestResponse,
|
|
|
|
tags=['webserver', 'backtest'])
|
2023-06-01 05:07:02 +00:00
|
|
|
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
|
2022-04-11 18:04:47 +00:00
|
|
|
# Get backtest result history, read from metadata files
|
2023-07-25 18:34:45 +00:00
|
|
|
bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
|
2023-07-25 18:38:49 +00:00
|
|
|
fn = (bt_results_base / filename).with_suffix('.json')
|
|
|
|
|
2022-04-13 04:55:47 +00:00
|
|
|
results: Dict[str, Any] = {
|
2022-04-13 04:47:39 +00:00
|
|
|
'metadata': {},
|
|
|
|
'strategy': {},
|
|
|
|
'strategy_comparison': [],
|
|
|
|
}
|
2023-07-25 18:20:09 +00:00
|
|
|
if not is_file_in_dir(fn, bt_results_base):
|
|
|
|
raise HTTPException(status_code=404, detail="File not found.")
|
2022-04-13 04:47:39 +00:00
|
|
|
load_and_merge_backtest_result(strategy, fn, results)
|
2022-04-11 18:04:47 +00:00
|
|
|
return {
|
|
|
|
"status": "ended",
|
|
|
|
"running": False,
|
|
|
|
"step": "",
|
|
|
|
"progress": 1,
|
|
|
|
"status_msg": "Historic result",
|
2022-04-13 04:47:39 +00:00
|
|
|
"backtest_result": results,
|
2022-04-11 18:04:47 +00:00
|
|
|
}
|
2023-07-25 18:34:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
@router.delete('/backtest/history/{file}', response_model=List[BacktestHistoryEntry],
|
|
|
|
tags=['webserver', 'backtest'])
|
|
|
|
def api_delete_backtest_history_entry(file: str, config=Depends(get_config)):
|
|
|
|
# Get backtest result history, read from metadata files
|
|
|
|
bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
|
2023-07-25 18:38:49 +00:00
|
|
|
file_abs = (bt_results_base / file).with_suffix('.json')
|
2023-07-25 18:34:45 +00:00
|
|
|
# Ensure file is in backtest_results directory
|
|
|
|
if not is_file_in_dir(file_abs, bt_results_base):
|
|
|
|
raise HTTPException(status_code=404, detail="File not found.")
|
|
|
|
|
2023-07-25 18:42:01 +00:00
|
|
|
delete_backtest_result(file_abs)
|
2023-07-25 18:34:45 +00:00
|
|
|
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
|
2023-07-31 19:16:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
@router.patch('/backtest/history/{file}', response_model=List[BacktestHistoryEntry],
|
|
|
|
tags=['webserver', 'backtest'])
|
|
|
|
def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate,
|
|
|
|
config=Depends(get_config)):
|
|
|
|
# Get backtest result history, read from metadata files
|
|
|
|
bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
|
|
|
|
file_abs = (bt_results_base / file).with_suffix('.json')
|
|
|
|
# Ensure file is in backtest_results directory
|
|
|
|
if not is_file_in_dir(file_abs, bt_results_base):
|
|
|
|
raise HTTPException(status_code=404, detail="File not found.")
|
|
|
|
content = {
|
|
|
|
'notes': body.notes
|
|
|
|
}
|
|
|
|
try:
|
|
|
|
update_backtest_metadata(file_abs, body.strategy, content)
|
|
|
|
except ValueError as e:
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
|
|
return get_backtest_result(file_abs)
|