freqtrade_origin/freqtrade/rpc/api_server/api_backtest.py

355 lines
12 KiB
Python
Raw Normal View History

2021-04-01 05:55:29 +00:00
import asyncio
import logging
from copy import deepcopy
from datetime import datetime
2023-07-25 18:34:45 +00:00
from pathlib import Path
from typing import Any, Dict, List
2021-04-01 05:55:29 +00:00
from fastapi import APIRouter, BackgroundTasks, Depends
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
from freqtrade.constants import Config
from freqtrade.data.btanalysis import (
delete_backtest_result,
get_backtest_market_change,
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
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
2023-05-13 13:38:40 +00:00
from freqtrade.exchange.common import remove_exchange_credentials
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
from freqtrade.rpc.api_server.api_schemas import (
BacktestHistoryEntry,
BacktestMarketChange,
BacktestMetadataUpdate,
BacktestRequest,
BacktestResponse,
)
2023-06-01 05:07:02 +00:00
from freqtrade.rpc.api_server.deps import get_config
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()
def __run_backtest_bg(btconfig: Config):
from freqtrade.data.metrics import combined_dataframes_with_rel_mean
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
2024-05-12 14:51:11 +00:00
lastconfig = ApiBG.bt["last_config"]
strat = StrategyResolver.load_strategy(btconfig)
validate_config_consistency(btconfig)
if (
2024-05-12 14:51:11 +00:00
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
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt"] = Backtesting(btconfig)
ApiBG.bt["bt"].load_bt_data_detail()
else:
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt"].config = btconfig
ApiBG.bt["bt"].init_backtest()
# Only reload data if timeframe changed.
if (
2024-05-12 14:51:11 +00:00
not ApiBG.bt["data"]
or not ApiBG.bt["timerange"]
or lastconfig.get("timeframe") != strat.timeframe
or lastconfig.get("timerange") != btconfig["timerange"]
):
2024-05-12 14:51:11 +00:00
ApiBG.bt["data"], ApiBG.bt["timerange"] = ApiBG.bt["bt"].load_bt_data()
2024-05-12 14:51:11 +00:00
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")
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt"].enable_protections = btconfig.get("enable_protections", False)
ApiBG.bt["bt"].strategylist = [strat]
ApiBG.bt["bt"].results = get_BacktestResultType_default()
ApiBG.bt["bt"].load_prior_backtest()
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt"].abort = False
strategy_name = strat.get_strategy_name()
2024-05-12 14:51:11 +00:00
if ApiBG.bt["bt"].results and strategy_name in ApiBG.bt["bt"].results["strategy"]:
# When previous result hash matches - reuse that result and skip backtesting.
2024-05-12 14:51:11 +00:00
logger.info(f"Reusing result of previous backtest for {strategy_name}")
else:
2024-05-12 14:51:11 +00:00
min_date, max_date = ApiBG.bt["bt"].backtest_one_strategy(
strat, ApiBG.bt["data"], ApiBG.bt["timerange"]
)
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt"].results = generate_backtest_stats(
ApiBG.bt["data"], ApiBG.bt["bt"].all_results, min_date=min_date, max_date=max_date
)
2024-05-12 14:51:11 +00:00
if btconfig.get("export", "none") == "trades":
combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date)
fn = store_backtest_stats(
2024-05-12 14:51:11 +00:00
btconfig["exportfilename"],
ApiBG.bt["bt"].results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
2024-05-12 14:51:11 +00:00
market_change_data=combined_res,
)
ApiBG.bt["bt"].results["metadata"][strategy_name]["filename"] = str(fn.stem)
ApiBG.bt["bt"].results["metadata"][strategy_name]["strategy"] = strategy_name
logger.info("Backtest finished.")
except ConfigurationError as e:
logger.error(f"Backtesting encountered a configuration Error: {e}")
except (Exception, OperationalException, DependencyException) as e:
logger.exception(f"Backtesting caused an error: {e}")
2024-05-12 14:51:11 +00:00
ApiBG.bt["bt_error"] = str(e)
finally:
ApiBG.bgtask_running = False
2024-05-12 14:51:11 +00:00
@router.post("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"])
async def api_start_backtest(
2024-05-12 14:51:11 +00:00
bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config)
):
ApiBG.bt["bt_error"] = None
2021-04-01 05:55:29 +00:00
"""Start backtesting if not done so already"""
if ApiBG.bgtask_running:
2024-05-12 14:51:11 +00:00
raise RPCException("Bot Background task already running")
2021-04-01 05:55:29 +00:00
2024-05-12 14:51:11 +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)
2024-05-12 14:51:11 +00:00
remove_exchange_credentials(btconfig["exchange"], True)
2021-04-01 05:55:29 +00:00
settings = dict(bt_settings)
2024-05-12 14:51:11 +00:00
if settings.get("freqai", None) is not None:
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)
try:
2024-05-12 14:51:11 +00:00
btconfig["stake_amount"] = float(btconfig["stake_amount"])
except ValueError:
pass
2021-04-01 05:55:29 +00:00
# Force dry-run for backtesting
2024-05-12 14:51:11 +00:00
btconfig["dry_run"] = True
2021-04-01 05:55:29 +00:00
# Start backtesting
# Initialize backtesting object
background_tasks.add_task(__run_backtest_bg, btconfig=btconfig)
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",
}
2024-05-12 14:51:11 +00:00
@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
2024-05-12 14:51:11 +00:00
if ApiBG.bgtask_running:
2021-04-01 05:55:29 +00:00
return {
"status": "running",
"running": True,
2024-05-12 14:51:11 +00:00
"step": (
ApiBG.bt["bt"].progress.action if ApiBG.bt["bt"] else str(BacktestState.STARTUP)
),
"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",
}
2024-05-12 14:51:11 +00:00
if not ApiBG.bt["bt"]:
2021-04-01 05:55:29 +00:00
return {
"status": "not_started",
"running": False,
"step": "",
"progress": 0,
2024-05-12 14:51:11 +00:00
"status_msg": "Backtest not yet executed",
2021-04-01 05:55:29 +00:00
}
2024-05-12 14:51:11 +00:00
if ApiBG.bt["bt_error"]:
2023-02-22 19:22:59 +00:00
return {
"status": "error",
"running": False,
"step": "",
"progress": 0,
2024-05-12 14:51:11 +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,
2024-05-12 14:51:11 +00:00
"backtest_result": ApiBG.bt["bt"].results,
2021-04-01 05:55:29 +00:00
}
2024-05-12 14:51:11 +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"""
if ApiBG.bgtask_running:
2021-04-01 05:55:29 +00:00
return {
"status": "running",
"running": True,
"step": "",
"progress": 0,
"status_msg": "Backtest running",
}
2024-05-12 14:51:11 +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
2024-05-12 14:51:11 +00:00
@router.get("/backtest/abort", response_model=BacktestResponse, tags=["webserver", "backtest"])
2023-06-01 05:07:02 +00:00
def api_backtest_abort():
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",
}
2024-05-12 14:51:11 +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",
}
2024-05-12 14:51:11 +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)):
# Get backtest result history, read from metadata files
2024-05-12 14:51:11 +00:00
return get_backtest_resultlist(config["user_data_dir"] / "backtest_results")
2024-05-12 14:51:11 +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)):
# Get backtest result history, read from metadata files
2024-05-12 14:51:11 +00:00
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
fn = (bt_results_base / filename).with_suffix(".json")
2023-07-25 18:38:49 +00:00
results: Dict[str, Any] = {
2024-05-12 14:51:11 +00:00
"metadata": {},
"strategy": {},
"strategy_comparison": [],
2022-04-13 04:47:39 +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)
return {
"status": "ended",
"running": False,
"step": "",
"progress": 1,
"status_msg": "Historic result",
2022-04-13 04:47:39 +00:00
"backtest_result": results,
}
2023-07-25 18:34:45 +00:00
2024-05-12 14:51:11 +00:00
@router.delete(
"/backtest/history/{file}",
response_model=List[BacktestHistoryEntry],
tags=["webserver", "backtest"],
)
2023-07-25 18:34:45 +00:00
def api_delete_backtest_history_entry(file: str, config=Depends(get_config)):
# Get backtest result history, read from metadata files
2024-05-12 14:51:11 +00:00
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
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.")
delete_backtest_result(file_abs)
2024-05-12 14:51:11 +00:00
return get_backtest_resultlist(config["user_data_dir"] / "backtest_results")
2023-07-31 19:16:25 +00:00
2024-05-12 14:51:11 +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)
):
2023-07-31 19:16:25 +00:00
# Get backtest result history, read from metadata files
2024-05-12 14:51:11 +00:00
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
file_abs = (bt_results_base / file).with_suffix(".json")
2023-07-31 19:16:25 +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.")
2024-05-12 14:51:11 +00:00
content = {"notes": body.notes}
2023-07-31 19:16:25 +00:00
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)
2024-05-12 14:51:11 +00:00
@router.get(
"/backtest/history/{file}/market_change",
response_model=BacktestMarketChange,
tags=["webserver", "backtest"],
)
def api_get_backtest_market_change(file: str, config=Depends(get_config)):
2024-05-12 14:51:11 +00:00
bt_results_base: Path = config["user_data_dir"] / "backtest_results"
file_abs = (bt_results_base / f"{file}_market_change").with_suffix(".feather")
# 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.")
df = get_backtest_market_change(file_abs)
return {
2024-05-12 14:51:11 +00:00
"columns": df.columns.tolist(),
"data": df.values.tolist(),
"length": len(df),
}