mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 01:21:11 +00:00
Merge pull request #10485 from jainanuj94/feature/8902
Add exit signals to export in backtesting
This commit is contained in:
commit
611a3ce138
|
@ -18,15 +18,13 @@ freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_nam
|
|||
```
|
||||
|
||||
This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding
|
||||
DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy
|
||||
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
|
||||
folder to delete old exports.
|
||||
DataFrame of the candles that resulted in entry and exit signals.
|
||||
Depending on how many entries your strategy makes, this file may get quite large, so periodically check your `user_data/backtest_results` folder to delete old exports.
|
||||
|
||||
Before running your next backtest, make sure you either delete your old backtest results or run
|
||||
backtesting with the `--cache none` option to make sure no cached results are used.
|
||||
|
||||
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
|
||||
`user_data/backtest_results` folder.
|
||||
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` and `backtest-result-{timestamp}_exited.pkl` files in the `user_data/backtest_results` folder.
|
||||
|
||||
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||
with `--analysis-groups` option provided with space-separated arguments:
|
||||
|
@ -103,6 +101,10 @@ The indicators have to be present in your strategy's main DataFrame (either for
|
|||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||
output.
|
||||
|
||||
!!! Note "Indicator List"
|
||||
The indicator values will be displayed for both entry and exit points. If `--indicator-list all` is specified,
|
||||
only the indicators at the entry point will be shown to avoid excessively large lists, which could occur depending on the strategy.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
|
@ -118,6 +120,34 @@ automatically accessible by including them on the indicator-list, and these incl
|
|||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
|
||||
#### Sample Output for Indicator Values
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
||||
```
|
||||
|
||||
In this example,
|
||||
we aim to display the `chikou_span` and `tenkan_sen` indicator values at both the entry and exit points of trades.
|
||||
|
||||
A sample output for indicators might look like this:
|
||||
|
||||
| pair | open_date | enter_reason | exit_reason | chikou_span (entry) | tenkan_sen (entry) | chikou_span (exit) | tenkan_sen (exit) |
|
||||
|-----------|---------------------------|--------------|-------------|---------------------|--------------------|--------------------|-------------------|
|
||||
| DOGE/USDT | 2024-07-06 00:35:00+00:00 | | exit_signal | 0.105 | 0.106 | 0.105 | 0.107 |
|
||||
| BTC/USDT | 2024-08-05 14:20:00+00:00 | | roi | 54643.440 | 51696.400 | 54386.000 | 52072.010 |
|
||||
|
||||
As shown in the table, `chikou_span (entry)` represents the indicator value at the time of trade entry,
|
||||
while `chikou_span (exit)` reflects its value at the time of exit.
|
||||
This detailed view of indicator values enhances the analysis.
|
||||
|
||||
The `(entry)` and `(exit)` suffixes are added to indicators
|
||||
to distinguish the values at the entry and exit points of the trade.
|
||||
|
||||
!!! Note "Trade-wide Indicators"
|
||||
Certain trade-wide indicators do not have the `(entry)` or `(exit)` suffix. These indicators include: `pair`, `stake_amount`,
|
||||
`max_stake_amount`, `amount`, `open_date`, `close_date`, `open_rate`, `close_rate`, `fee_open`, `fee_close`, `trade_duration`,
|
||||
`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 the trade output by date
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Dict, List
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
|
@ -8,6 +8,7 @@ import pandas as pd
|
|||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.btanalysis import (
|
||||
BT_DATA_COLUMNS,
|
||||
get_latest_backtest_filename,
|
||||
load_backtest_data,
|
||||
load_backtest_stats,
|
||||
|
@ -47,9 +48,14 @@ def _load_signal_candles(backtest_dir: Path):
|
|||
return _load_backtest_analysis_data(backtest_dir, "signals")
|
||||
|
||||
|
||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||
analysed_trades_dict = {}
|
||||
analysed_trades_dict[strategy_name] = {}
|
||||
def _load_exit_signal_candles(backtest_dir: Path) -> Dict[str, Dict[str, pd.DataFrame]]:
|
||||
return _load_backtest_analysis_data(backtest_dir, "exited")
|
||||
|
||||
|
||||
def _process_candles_and_indicators(
|
||||
pairlist, strategy_name, trades, signal_candles, date_col: str = "open_date"
|
||||
):
|
||||
analysed_trades_dict: Dict[str, Dict] = {strategy_name: {}}
|
||||
|
||||
try:
|
||||
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
|
||||
|
@ -57,7 +63,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||
for pair in pairlist:
|
||||
if pair in signal_candles[strategy_name]:
|
||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||
pair, trades, signal_candles[strategy_name][pair]
|
||||
pair, trades, signal_candles[strategy_name][pair], date_col
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||
|
@ -65,7 +71,9 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||
return analysed_trades_dict
|
||||
|
||||
|
||||
def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
|
||||
def _analyze_candles_and_indicators(
|
||||
pair: str, trades: pd.DataFrame, signal_candles: pd.DataFrame, date_col: str = "open_date"
|
||||
) -> pd.DataFrame:
|
||||
buyf = signal_candles
|
||||
|
||||
if len(buyf) > 0:
|
||||
|
@ -75,8 +83,8 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
|||
trades_inds = pd.DataFrame()
|
||||
|
||||
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
||||
for t, v in trades_red.open_date.items():
|
||||
allinds = buyf.loc[(buyf["date"] < v)]
|
||||
for t, v in trades_red.iterrows():
|
||||
allinds = buyf.loc[(buyf["date"] < v[date_col])]
|
||||
if allinds.shape[0] > 0:
|
||||
tmp_inds = allinds.iloc[[-1]]
|
||||
|
||||
|
@ -235,7 +243,7 @@ def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
|||
|
||||
def prepare_results(
|
||||
analysed_trades, stratname, enter_reason_list, exit_reason_list, timerange=None
|
||||
):
|
||||
) -> pd.DataFrame:
|
||||
res_df = pd.DataFrame()
|
||||
for pair, trades in analysed_trades[stratname].items():
|
||||
if trades.shape[0] > 0:
|
||||
|
@ -252,6 +260,7 @@ def prepare_results(
|
|||
|
||||
def print_results(
|
||||
res_df: pd.DataFrame,
|
||||
exit_df: pd.DataFrame,
|
||||
analysis_groups: List[str],
|
||||
indicator_list: List[str],
|
||||
csv_path: Path,
|
||||
|
@ -278,9 +287,11 @@ def print_results(
|
|||
for ind in indicator_list:
|
||||
if ind in res_df:
|
||||
available_inds.append(ind)
|
||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||
|
||||
merged_df = _merge_dfs(res_df, exit_df, available_inds)
|
||||
|
||||
_print_table(
|
||||
res_df[ilist],
|
||||
merged_df,
|
||||
sortcols=["exit_reason"],
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
|
@ -291,6 +302,22 @@ def print_results(
|
|||
print("\\No trades to show")
|
||||
|
||||
|
||||
def _merge_dfs(entry_df, exit_df, available_inds):
|
||||
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
|
||||
|
||||
if exit_df is None or exit_df.empty:
|
||||
return entry_df[columns_to_keep]
|
||||
|
||||
return pd.merge(
|
||||
entry_df[columns_to_keep],
|
||||
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
|
||||
):
|
||||
|
@ -333,6 +360,7 @@ def process_entry_exit_reasons(config: Config):
|
|||
|
||||
if trades is not None and not trades.empty:
|
||||
signal_candles = _load_signal_candles(config["exportfilename"])
|
||||
exit_signals = _load_exit_signal_candles(config["exportfilename"])
|
||||
|
||||
rej_df = None
|
||||
if do_rejected:
|
||||
|
@ -345,20 +373,31 @@ def process_entry_exit_reasons(config: Config):
|
|||
timerange=timerange,
|
||||
)
|
||||
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
config["exchange"]["pair_whitelist"], strategy_name, trades, signal_candles
|
||||
)
|
||||
|
||||
res_df = prepare_results(
|
||||
analysed_trades_dict,
|
||||
strategy_name,
|
||||
entry_df = _generate_dfs(
|
||||
config["exchange"]["pair_whitelist"],
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
timerange=timerange,
|
||||
signal_candles,
|
||||
strategy_name,
|
||||
timerange,
|
||||
trades,
|
||||
"open_date",
|
||||
)
|
||||
|
||||
exit_df = _generate_dfs(
|
||||
config["exchange"]["pair_whitelist"],
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
exit_signals,
|
||||
strategy_name,
|
||||
timerange,
|
||||
trades,
|
||||
"close_date",
|
||||
)
|
||||
|
||||
print_results(
|
||||
res_df,
|
||||
entry_df,
|
||||
exit_df,
|
||||
analysis_groups,
|
||||
indicator_list,
|
||||
rejected_signals=rej_df,
|
||||
|
@ -368,3 +407,30 @@ def process_entry_exit_reasons(config: Config):
|
|||
|
||||
except ValueError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
||||
def _generate_dfs(
|
||||
pairlist: list,
|
||||
enter_reason_list: list,
|
||||
exit_reason_list: list,
|
||||
signal_candles: Dict,
|
||||
strategy_name: str,
|
||||
timerange: TimeRange,
|
||||
trades: pd.DataFrame,
|
||||
date_col: str,
|
||||
) -> pd.DataFrame:
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
pairlist,
|
||||
strategy_name,
|
||||
trades,
|
||||
signal_candles,
|
||||
date_col,
|
||||
)
|
||||
res_df = prepare_results(
|
||||
analysed_trades_dict,
|
||||
strategy_name,
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
timerange=timerange,
|
||||
)
|
||||
return res_df
|
||||
|
|
|
@ -122,6 +122,7 @@ class Backtesting:
|
|||
self.processed_dfs: Dict[str, Dict] = {}
|
||||
self.rejected_dict: Dict[str, List] = {}
|
||||
self.rejected_df: Dict[str, Dict] = {}
|
||||
self.exited_dfs: Dict[str, Dict] = {}
|
||||
|
||||
self._exchange_name = self.config["exchange"]["name"]
|
||||
if not exchange:
|
||||
|
@ -1564,11 +1565,14 @@ class Backtesting:
|
|||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
self.processed_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results
|
||||
preprocessed_tmp, results, "open_date"
|
||||
)
|
||||
self.rejected_df[strategy_name] = generate_rejected_signals(
|
||||
preprocessed_tmp, self.rejected_dict
|
||||
)
|
||||
self.exited_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results, "close_date"
|
||||
)
|
||||
|
||||
return min_date, max_date
|
||||
|
||||
|
@ -1644,7 +1648,11 @@ class Backtesting:
|
|||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
store_backtest_analysis_results(
|
||||
self.config["exportfilename"], self.processed_dfs, self.rejected_df, dt_appendix
|
||||
self.config["exportfilename"],
|
||||
self.processed_dfs,
|
||||
self.rejected_df,
|
||||
self.exited_dfs,
|
||||
dt_appendix,
|
||||
)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
|
|
|
@ -90,7 +90,12 @@ def _store_backtest_analysis_data(
|
|||
|
||||
|
||||
def store_backtest_analysis_results(
|
||||
recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], dtappendix: str
|
||||
recordfilename: Path,
|
||||
candles: Dict[str, Dict],
|
||||
trades: Dict[str, Dict],
|
||||
exited: Dict[str, Dict],
|
||||
dtappendix: str,
|
||||
) -> None:
|
||||
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
|
||||
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
|
||||
_store_backtest_analysis_data(recordfilename, exited, dtappendix, "exited")
|
||||
|
|
|
@ -25,8 +25,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def generate_trade_signal_candles(
|
||||
preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any]
|
||||
) -> DataFrame:
|
||||
preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any], date_col: str
|
||||
) -> Dict[str, DataFrame]:
|
||||
signal_candles_only = {}
|
||||
for pair in preprocessed_df.keys():
|
||||
signal_candles_only_df = DataFrame()
|
||||
|
@ -36,8 +36,8 @@ def generate_trade_signal_candles(
|
|||
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||
|
||||
if pairdf.shape[0] > 0:
|
||||
for t, v in pairresults.open_date.items():
|
||||
allinds = pairdf.loc[(pairdf["date"] < v)]
|
||||
for t, v in pairresults.iterrows():
|
||||
allinds = pairdf.loc[(pairdf["date"] < v[date_col])]
|
||||
signal_inds = allinds.iloc[[-1]]
|
||||
signal_candles_only_df = concat(
|
||||
[signal_candles_only_df.infer_objects(), signal_inds.infer_objects()]
|
||||
|
|
|
@ -18,7 +18,9 @@ def entryexitanalysis_cleanup() -> None:
|
|||
Backtesting.cleanup()
|
||||
|
||||
|
||||
def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, user_dir, capsys):
|
||||
def test_backtest_analysis_on_entry_and_rejected_signals_nomock(
|
||||
default_conf, mocker, caplog, testdatadir, user_dir, capsys
|
||||
):
|
||||
caplog.set_level(logging.INFO)
|
||||
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
@ -158,6 +160,15 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
|
|||
assert "34.049" in captured.out
|
||||
assert "0.104" in captured.out
|
||||
assert "52.829" in captured.out
|
||||
# assert indicator list
|
||||
assert "close (entry)" in captured.out
|
||||
assert "0.016" in captured.out
|
||||
assert "rsi (entry)" in captured.out
|
||||
assert "54.320" in captured.out
|
||||
assert "close (exit)" in captured.out
|
||||
assert "rsi (exit)" in captured.out
|
||||
assert "52.829" in captured.out
|
||||
assert "profit_abs" in captured.out
|
||||
|
||||
# test group 1
|
||||
args = get_args(base_args + ["--analysis-groups", "1"])
|
||||
|
|
|
@ -293,20 +293,25 @@ def test_store_backtest_candles(testdatadir, mocker):
|
|||
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
|
||||
|
||||
# mock directory exporting
|
||||
store_backtest_analysis_results(testdatadir, candle_dict, {}, "2022_01_01_15_05_13")
|
||||
store_backtest_analysis_results(testdatadir, candle_dict, {}, {}, "2022_01_01_15_05_13")
|
||||
|
||||
assert dump_mock.call_count == 2
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
|
||||
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
|
||||
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
|
||||
|
||||
dump_mock.reset_mock()
|
||||
# mock file exporting
|
||||
filename = Path(testdatadir / "testresult")
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, "2022_01_01_15_05_13")
|
||||
assert dump_mock.call_count == 2
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, {}, "2022_01_01_15_05_13")
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
||||
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
|
||||
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
|
||||
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
|
||||
|
||||
dump_mock.reset_mock()
|
||||
|
||||
|
||||
|
@ -315,7 +320,7 @@ def test_write_read_backtest_candles(tmp_path):
|
|||
|
||||
# test directory exporting
|
||||
sample_date = "2022_01_01_15_05_13"
|
||||
store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date)
|
||||
store_backtest_analysis_results(tmp_path, candle_dict, {}, {}, sample_date)
|
||||
stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
@ -330,7 +335,7 @@ def test_write_read_backtest_candles(tmp_path):
|
|||
|
||||
# test file exporting
|
||||
filename = tmp_path / "testresult"
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, {}, sample_date)
|
||||
stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
|
Loading…
Reference in New Issue
Block a user