Merge pull request #10639 from jainanuj94/backtesting

Add entry-only and exit-only filters to --indicator-list in backtesting analysis
This commit is contained in:
Matthias 2024-09-12 06:43:24 +02:00 committed by GitHub
commit e96928588e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 364 additions and 6 deletions

View File

@ -149,6 +149,25 @@ to distinguish the values at the entry and exit points of the trade.
`profit_ratio`, `profit_abs`, `exit_reason`,`initial_stop_loss_abs`, `initial_stop_loss_ratio`, `stop_loss_abs`, `stop_loss_ratio`,
`min_rate`, `max_rate`, `is_open`, `enter_tag`, `leverage`, `is_short`, `open_timestamp`, `close_timestamp` and `orders`
#### Filtering Indicators Based on Entry or Exit Signals
The `--indicator-list` option, by default, displays indicator values for both entry and exit signals. To filter the indicator values exclusively for entry signals, you can use the `--entry-only` argument. Similarly, to display indicator values only at exit signals, use the `--exit-only` argument.
Example: Display indicator values at entry signals:
```bash
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
```
Example: Display indicator values at exit signals:
```bash
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
```
!!! note
When using these filters, the indicator names will not be suffixed with `(entry)` or `(exit)`.
### Filtering the trade output by date
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:

View File

@ -228,6 +228,8 @@ ARGS_ANALYZE_ENTRIES_EXITS = [
"enter_reason_list",
"exit_reason_list",
"indicator_list",
"entry_only",
"exit_only",
"timerange",
"analysis_rejected",
"analysis_to_csv",

View File

@ -719,6 +719,12 @@ AVAILABLE_CLI_OPTIONS = {
nargs="+",
default=[],
),
"entry_only": Arg(
"--entry-only", help=("Only analyze entry signals."), action="store_true", default=False
),
"exit_only": Arg(
"--exit-only", help=("Only analyze exit signals."), action="store_true", default=False
),
"analysis_rejected": Arg(
"--rejected-signals",
help="Analyse rejected signals",

View File

@ -407,6 +407,8 @@ class Configuration:
("enter_reason_list", "Analysis enter tag list: {}"),
("exit_reason_list", "Analysis exit tag list: {}"),
("indicator_list", "Analysis indicator list: {}"),
("entry_only", "Only analyze entry signals: {}"),
("exit_only", "Only analyze exit signals: {}"),
("timerange", "Filter trades by timerange: {}"),
("analysis_rejected", "Analyse rejected signals: {}"),
("analysis_to_csv", "Store analysis tables to CSV: {}"),

View File

@ -263,6 +263,8 @@ def print_results(
exit_df: pd.DataFrame,
analysis_groups: List[str],
indicator_list: List[str],
entry_only: bool,
exit_only: bool,
csv_path: Path,
rejected_signals=None,
to_csv=False,
@ -288,7 +290,7 @@ def print_results(
if ind in res_df:
available_inds.append(ind)
merged_df = _merge_dfs(res_df, exit_df, available_inds)
merged_df = _merge_dfs(res_df, exit_df, available_inds, entry_only, exit_only)
_print_table(
merged_df,
@ -302,14 +304,21 @@ def print_results(
print("\\No trades to show")
def _merge_dfs(entry_df, exit_df, available_inds):
def _merge_dfs(
entry_df: pd.DataFrame,
exit_df: pd.DataFrame,
available_inds: List[str],
entry_only: bool,
exit_only: bool,
):
merge_on = ["pair", "open_date"]
signal_wide_indicators = list(set(available_inds) - set(BT_DATA_COLUMNS))
columns_to_keep = merge_on + ["enter_reason", "exit_reason"] + available_inds
columns_to_keep = merge_on + ["enter_reason", "exit_reason"]
if exit_df is None or exit_df.empty:
return entry_df[columns_to_keep]
if exit_df is None or exit_df.empty or entry_only is True:
return entry_df[columns_to_keep + available_inds]
if exit_only is True:
return pd.merge(
entry_df[columns_to_keep],
exit_df[merge_on + signal_wide_indicators],
@ -317,6 +326,13 @@ def _merge_dfs(entry_df, exit_df, available_inds):
suffixes=(" (entry)", " (exit)"),
)
return pd.merge(
entry_df[columns_to_keep + available_inds],
exit_df[merge_on + signal_wide_indicators],
on=merge_on,
suffixes=(" (entry)", " (exit)"),
)
def _print_table(
df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, to_csv=False, csv_path: Path
@ -343,9 +359,16 @@ def process_entry_exit_reasons(config: Config):
enter_reason_list = config.get("enter_reason_list", ["all"])
exit_reason_list = config.get("exit_reason_list", ["all"])
indicator_list = config.get("indicator_list", [])
entry_only = config.get("entry_only", False)
exit_only = config.get("exit_only", False)
do_rejected = config.get("analysis_rejected", False)
to_csv = config.get("analysis_to_csv", False)
csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
if entry_only is True and exit_only is True:
raise OperationalException(
"Cannot use --entry-only and --exit-only at the same time. Please choose one."
)
if to_csv and not csv_path.is_dir():
raise OperationalException(f"Specified directory {csv_path} does not exist.")
@ -400,6 +423,8 @@ def process_entry_exit_reasons(config: Config):
exit_df,
analysis_groups,
indicator_list,
entry_only,
exit_only,
rejected_signals=rej_df,
to_csv=to_csv,
csv_path=csv_path,

View File

@ -7,6 +7,7 @@ import pytest
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.optimize_commands import start_backtesting
from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import get_args, patch_exchange, patched_configuration_load_config_file
@ -256,3 +257,306 @@ def test_backtest_analysis_on_entry_and_rejected_signals_nomock(
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert "no rejected signals" in captured.out
def test_backtest_analysis_with_invalid_config(
default_conf, mocker, caplog, testdatadir, user_dir, capsys
):
caplog.set_level(logging.INFO)
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
default_conf.update(
{
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
}
)
patch_exchange(mocker)
result1 = pd.DataFrame(
{
"pair": ["ETH/BTC", "LTC/BTC", "ETH/BTC", "LTC/BTC"],
"profit_ratio": [0.025, 0.05, -0.1, -0.05],
"profit_abs": [0.5, 2.0, -4.0, -2.0],
"open_date": pd.to_datetime(
[
"2018-01-29 18:40:00",
"2018-01-30 03:30:00",
"2018-01-30 08:10:00",
"2018-01-31 13:30:00",
],
utc=True,
),
"close_date": pd.to_datetime(
[
"2018-01-29 20:45:00",
"2018-01-30 05:35:00",
"2018-01-30 09:10:00",
"2018-01-31 15:00:00",
],
utc=True,
),
"trade_duration": [235, 40, 60, 90],
"is_open": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"open_rate": [0.104445, 0.10302485, 0.10302485, 0.10302485],
"close_rate": [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
"enter_tag": [
"enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b",
],
"exit_reason": [
ExitType.ROI.value,
ExitType.EXIT_SIGNAL.value,
ExitType.STOP_LOSS.value,
ExitType.TRAILING_STOP_LOSS.value,
],
}
)
backtestmock = MagicMock(
side_effect=[
{
"results": result1,
"config": default_conf,
"locks": [],
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"final_balance": 1000,
}
]
)
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["ETH/BTC", "LTC/BTC", "DASH/BTC"]),
)
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest", backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
"backtesting",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
"--timeframe",
"5m",
"--timerange",
"1515560100-1517287800",
"--export",
"signals",
"--cache",
"none",
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
assert "EXIT REASON STATS" in captured.out
assert "LEFT OPEN TRADES REPORT" in captured.out
base_args = [
"backtesting-analysis",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
]
# test with both entry and exit only arguments
args = get_args(
base_args
+ [
"--analysis-groups",
"0",
"--indicator-list",
"close",
"rsi",
"profit_abs",
"--entry-only",
"--exit-only",
]
)
with pytest.raises(
OperationalException,
match=r"Cannot use --entry-only and --exit-only at the same time. Please choose one.",
):
start_analysis_entries_exits(args)
def test_backtest_analysis_on_entry_and_rejected_signals_only_entry_signals(
default_conf, mocker, caplog, testdatadir, user_dir, capsys
):
caplog.set_level(logging.INFO)
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
default_conf.update(
{
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
}
)
patch_exchange(mocker)
result1 = pd.DataFrame(
{
"pair": ["ETH/BTC", "LTC/BTC", "ETH/BTC", "LTC/BTC"],
"profit_ratio": [0.025, 0.05, -0.1, -0.05],
"profit_abs": [0.5, 2.0, -4.0, -2.0],
"open_date": pd.to_datetime(
[
"2018-01-29 18:40:00",
"2018-01-30 03:30:00",
"2018-01-30 08:10:00",
"2018-01-31 13:30:00",
],
utc=True,
),
"close_date": pd.to_datetime(
[
"2018-01-29 20:45:00",
"2018-01-30 05:35:00",
"2018-01-30 09:10:00",
"2018-01-31 15:00:00",
],
utc=True,
),
"trade_duration": [235, 40, 60, 90],
"is_open": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"open_rate": [0.104445, 0.10302485, 0.10302485, 0.10302485],
"close_rate": [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
"enter_tag": [
"enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b",
],
"exit_reason": [
ExitType.ROI.value,
ExitType.EXIT_SIGNAL.value,
ExitType.STOP_LOSS.value,
ExitType.TRAILING_STOP_LOSS.value,
],
}
)
backtestmock = MagicMock(
side_effect=[
{
"results": result1,
"config": default_conf,
"locks": [],
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"final_balance": 1000,
}
]
)
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["ETH/BTC", "LTC/BTC", "DASH/BTC"]),
)
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest", backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
"backtesting",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
"--timeframe",
"5m",
"--timerange",
"1515560100-1517287800",
"--export",
"signals",
"--cache",
"none",
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
assert "EXIT REASON STATS" in captured.out
assert "LEFT OPEN TRADES REPORT" in captured.out
base_args = [
"backtesting-analysis",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
]
# test group 0 and indicator list
args = get_args(
base_args
+ [
"--analysis-groups",
"0",
"--indicator-list",
"close",
"rsi",
"profit_abs",
"--entry-only",
]
)
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert "LTC/BTC" in captured.out
assert "ETH/BTC" in captured.out
assert "enter_tag_long_a" in captured.out
assert "enter_tag_long_b" in captured.out
assert "exit_signal" in captured.out
assert "roi" in captured.out
assert "stop_loss" in captured.out
assert "trailing_stop_loss" in captured.out
assert "0.5" in captured.out
assert "-4" in captured.out
assert "-2" in captured.out
assert "-3.5" in captured.out
assert "50" in captured.out
assert "0" in captured.out
assert "0.016" in captured.out
assert "34.049" in captured.out
assert "0.104" in captured.out
assert "52.829" in captured.out
# assert indicator list
assert "close" in captured.out
assert "close (entry)" not in captured.out
assert "0.016" in captured.out
assert "rsi (entry)" not in captured.out
assert "rsi" in captured.out
assert "54.320" in captured.out
assert "close (exit)" not in captured.out
assert "rsi (exit)" not in captured.out
assert "52.829" in captured.out
assert "profit_abs" in captured.out