diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 5c2500f18..ae3eb2e4e 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -100,3 +100,17 @@ freqtrade backtesting-analysis -c --analysis-groups 0 2 --enter-re The indicators have to be present in your strategy's main DataFrame (either for your main timeframe or for informative timeframes) otherwise they will simply be ignored in the script output. + +### 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: + +``` +--timerange : Timerange to filter output trades, start date inclusive, end date exclusive. e.g. 20220101-20221231 +``` + +For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January: + +```bash +freqtrade backtesting-analysis -c --timerange 20220101-20220201 +``` diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 926ed3eae..7a6c6bb96 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -83,7 +83,7 @@ from pathlib import Path project_root = "somedir/freqtrade" i=0 try: - os.chdirdir(project_root) + os.chdir(project_root) assert Path('LICENSE').is_file() except: while i<4 and (not Path('LICENSE').is_file()): diff --git a/docs/utils.md b/docs/utils.md index 3d8a3bd03..e717a0f9c 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -722,6 +722,7 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V] [--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]] [--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]] [--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]] + [--timerange YYYYMMDD-[YYYYMMDD]] optional arguments: -h, --help show this help message and exit @@ -744,6 +745,10 @@ optional arguments: --indicator-list INDICATOR_LIST [INDICATOR_LIST ...] Comma separated list of indicators to analyse. e.g. 'close,rsi,bb_lowerband,profit_abs' + --timerange YYYYMMDD-[YYYYMMDD] + Timerange to filter trades for analysis, + start inclusive, end exclusive. e.g. + 20220101-20220201 Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index b6b790788..20afa7ffd 100755 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -60,10 +60,4 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None: logger.info('Starting freqtrade in analysis mode') - process_entry_exit_reasons(config['exportfilename'], - config['exchange']['pair_whitelist'], - config['analysis_groups'], - config['enter_reason_list'], - config['exit_reason_list'], - config['indicator_list'] - ) + process_entry_exit_reasons(config) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 79ab9dafa..b53a1022d 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -106,7 +106,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "disableparamexport", "backtest_breakdown"] ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list", - "exit_reason_list", "indicator_list"] + "exit_reason_list", "indicator_list", "timerange"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-freqaimodels", diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4929c023d..664610f33 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -462,6 +462,9 @@ class Configuration: self._args_to_config(config, argname='indicator_list', logstring='Analysis indicator list: {}') + self._args_to_config(config, argname='timerange', + logstring='Filter trades by timerange: {}') + def _process_runmode(self, config: Config) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index b22c3f87e..565a279b1 100755 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,11 +1,12 @@ import logging from pathlib import Path -from typing import List, Optional import joblib import pandas as pd from tabulate import tabulate +from freqtrade.configuration import TimeRange +from freqtrade.constants import Config from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) from freqtrade.exceptions import OperationalException @@ -152,37 +153,55 @@ def _do_group_table_output(bigdf, glist): logger.warning("Invalid group mask specified.") -def _print_results(analysed_trades, stratname, analysis_groups, - enter_reason_list, exit_reason_list, - indicator_list, columns=None): - if columns is None: - columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason'] +def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'): + if timerange: + if timerange.starttype == 'date': + df = df.loc[(df[df_date_col] >= timerange.startdt)] + if timerange.stoptype == 'date': + df = df.loc[(df[df_date_col] < timerange.stopdt)] + return df - bigdf = pd.DataFrame() + +def _select_rows_by_tags(df, enter_reason_list, exit_reason_list): + if enter_reason_list and "all" not in enter_reason_list: + df = df.loc[(df['enter_reason'].isin(enter_reason_list))] + + if exit_reason_list and "all" not in exit_reason_list: + df = df.loc[(df['exit_reason'].isin(exit_reason_list))] + return df + + +def prepare_results(analysed_trades, stratname, + enter_reason_list, exit_reason_list, + timerange=None): + res_df = pd.DataFrame() for pair, trades in analysed_trades[stratname].items(): - bigdf = pd.concat([bigdf, trades], ignore_index=True) + res_df = pd.concat([res_df, trades], ignore_index=True) - if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns): + res_df = _select_rows_within_dates(res_df, timerange) + + if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns): + res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list) + + return res_df + + +def print_results(res_df, analysis_groups, indicator_list): + if res_df.shape[0] > 0: if analysis_groups: - _do_group_table_output(bigdf, analysis_groups) - - if enter_reason_list and "all" not in enter_reason_list: - bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))] - - if exit_reason_list and "all" not in exit_reason_list: - bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))] + _do_group_table_output(res_df, analysis_groups) if "all" in indicator_list: - print(bigdf) + print(res_df) elif indicator_list is not None: available_inds = [] for ind in indicator_list: - if ind in bigdf: + if ind in res_df: available_inds.append(ind) ilist = ["pair", "enter_reason", "exit_reason"] + available_inds - _print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False) + _print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False) else: - print("\\_ No trades to show") + print("\\No trades to show") def _print_table(df, sortcols=None, show_index=False): @@ -201,27 +220,34 @@ def _print_table(df, sortcols=None, show_index=False): ) -def process_entry_exit_reasons(backtest_dir: Path, - pairlist: List[str], - analysis_groups: Optional[List[str]] = ["0", "1", "2"], - enter_reason_list: Optional[List[str]] = ["all"], - exit_reason_list: Optional[List[str]] = ["all"], - indicator_list: Optional[List[str]] = []): +def process_entry_exit_reasons(config: Config): try: - backtest_stats = load_backtest_stats(backtest_dir) + analysis_groups = config.get('analysis_groups', []) + enter_reason_list = config.get('enter_reason_list', ["all"]) + exit_reason_list = config.get('exit_reason_list', ["all"]) + indicator_list = config.get('indicator_list', []) + + timerange = TimeRange.parse_timerange(None if config.get( + 'timerange') is None else str(config.get('timerange'))) + + backtest_stats = load_backtest_stats(config['exportfilename']) + for strategy_name, results in backtest_stats['strategy'].items(): - trades = load_backtest_data(backtest_dir, strategy_name) + trades = load_backtest_data(config['exportfilename'], strategy_name) if not trades.empty: - signal_candles = _load_signal_candles(backtest_dir) - analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name, - trades, signal_candles) - _print_results(analysed_trades_dict, - strategy_name, - analysis_groups, - enter_reason_list, - exit_reason_list, - indicator_list) + signal_candles = _load_signal_candles(config['exportfilename']) + 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, + enter_reason_list, exit_reason_list, + timerange=timerange) + + print_results(res_df, + analysis_groups, + indicator_list) except ValueError as e: raise OperationalException(e) from e diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 011543a09..334e18dc7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -740,6 +740,24 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} + def _force_entry_validations(self, pair: str, order_side: SignalDirection): + if not self._freqtrade.config.get('force_entry_enable', False): + raise RPCException('Force_entry not enabled.') + + if self._freqtrade.state != State.RUNNING: + raise RPCException('trader is not running') + + if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT: + raise RPCException("Can't go short on Spot markets.") + + if pair not in self._freqtrade.exchange.get_markets(tradable_only=True): + raise RPCException('Symbol does not exist or market is not active.') + # Check if pair quote currency equals to the stake currency. + stake_currency = self._freqtrade.config.get('stake_currency') + if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: + raise RPCException( + f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') + def _rpc_force_entry(self, pair: str, price: Optional[float], *, order_type: Optional[str] = None, order_side: SignalDirection = SignalDirection.LONG, @@ -750,21 +768,8 @@ class RPC: Handler for forcebuy Buys a pair trade at the given or current price """ + self._force_entry_validations(pair, order_side) - if not self._freqtrade.config.get('force_entry_enable', False): - raise RPCException('Force_entry not enabled.') - - if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') - - if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT: - raise RPCException("Can't go short on Spot markets.") - - # Check if pair quote currency equals to the stake currency. - stake_currency = self._freqtrade.config.get('stake_currency') - if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: - raise RPCException( - f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') # check if valid pair # check if pair already has an open pair diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 588220465..e33ed4955 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -189,3 +189,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '0.5' in captured.out assert '1' in captured.out assert '2.5' in captured.out + + # test date filtering + args = get_args(base_args + ['--timerange', "20180129-20180130"]) + start_analysis_entries_exits(args) + captured = capsys.readouterr() + assert 'enter_tag_long_a' in captured.out + assert 'enter_tag_long_b' not in captured.out diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8828b6f33..24b5f1cbe 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1056,6 +1056,10 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open assert trade.pair == pair assert trade.open_rate == 0.0001 + with pytest.raises(RPCException, + match=r'Symbol does not exist or market is not active.'): + rpc._rpc_force_entry('LTC/NOTHING', 0.0001) + # Test buy pair not with stakes with pytest.raises(RPCException, match=r'Wrong pair selected. Only pairs with stake-currency.*'):