diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 1143db394..a10ea5568 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -22,7 +22,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee", "pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", - "enable_protections", "dry_run_wallet", + "enable_protections", "dry_run_wallet", "detail_timeframe", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 215ed3f6e..a168a44cf 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -135,6 +135,10 @@ AVAILABLE_CLI_OPTIONS = { help='Override the value of the `stake_amount` configuration setting.', ), # Backtesting + "detail_timeframe": Arg( + '--timeframe-detail', + help='Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`).', + ), "position_stacking": Arg( '--eps', '--enable-position-stacking', help='Allow buying the same pair multiple times (position stacking).', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4dd5b7203..1d95dfb03 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -242,6 +242,9 @@ class Configuration: except ValueError: pass + self._args_to_config(config, argname='detail_timeframe', + logstring='Parameter --detail-timeframe detected, ' + 'using {} for intra-candle backtesting') self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eecc7af54..2106f76c6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -86,6 +86,16 @@ class Backtesting: "configuration or as cli argument `--timeframe 5m`") self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) + # Load detail timeframe if specified + self.timeframe_detail = str(self.config.get('detail_timeframe', '')) + if self.timeframe_detail: + self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail) + if self.timeframe_min <= self.timeframe_detail_min: + raise OperationalException( + "Detail timeframe must be smaller than strategy timeframe.") + + else: + self.timeframe_detail_min = 0 self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: @@ -158,7 +168,7 @@ class Backtesting: conf['protections'] = strategy.protections self.protections = ProtectionManager(self.config, strategy.protections) - def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: + def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange, Dict[str, DataFrame]]: """ Loads backtest data and returns the data combined with the timerange as tuple. @@ -174,6 +184,18 @@ class Backtesting: fail_without_data=True, data_format=self.config.get('dataformat_ohlcv', 'json'), ) + if self.timeframe_detail: + detail_data = history.load_data( + datadir=self.config['datadir'], + pairs=self.pairlists.whitelist, + timeframe=self.timeframe_detail, + timerange=self.timerange, + startup_candles=0, + fail_without_data=True, + data_format=self.config.get('dataformat_ohlcv', 'json'), + ) + else: + detail_data = None min_date, max_date = history.get_timerange(data) @@ -186,7 +208,7 @@ class Backtesting: self.required_startup, min_date) self.progress.set_new_value(1) - return data, self.timerange + return data, self.timerange, detail_data def prepare_backtest(self, enable_protections): """ @@ -318,7 +340,8 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, + sell_row: Tuple) -> Optional[LocalTrade]: sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_candle_time, sell_row[BUY_IDX], @@ -346,6 +369,29 @@ class Backtesting: return None + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + if self.timeframe_detail: + sell_candle_time = sell_row[DATE_IDX].to_pydatetime() + sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min) + + detail_data = self.detail_data[trade.pair] + detail_data = detail_data.loc[ + (detail_data['date'] >= sell_candle_time) & + (detail_data['date'] < sell_candle_end) + ] + detail_data['buy'] = sell_row[BUY_IDX] + detail_data['sell'] = sell_row[SELL_IDX] + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + for det_row in detail_data[headers].values.tolist(): + res = self._get_sell_trade_entry_for_candle(trade, det_row) + if res: + return res + + return None + + else: + return self._get_sell_trade_entry_for_candle(trade, sell_row) + def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) @@ -591,7 +637,7 @@ class Backtesting: """ data: Dict[str, Any] = {} - data, timerange = self.load_bt_data() + data, timerange, self.detail_data = self.load_bt_data() logger.info("Dataload complete. Calculating indicators") for strat in self.strategylist: