mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #9061 from freqtrade/fix/7389_backtest_startup_candle
improve `get_analyzed_dataframe` behavior in early candles
This commit is contained in:
@@ -17,7 +17,7 @@ from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWith
|
|||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
from freqtrade.exchange import Exchange, timeframe_to_prev_date, timeframe_to_seconds
|
||||||
from freqtrade.exchange.types import OrderBook
|
from freqtrade.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
@@ -46,6 +46,8 @@ class DataProvider:
|
|||||||
self.__rpc = rpc
|
self.__rpc = rpc
|
||||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||||
self.__slice_index: Optional[int] = None
|
self.__slice_index: Optional[int] = None
|
||||||
|
self.__slice_date: Optional[datetime] = None
|
||||||
|
|
||||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
self.__producer_pairs_df: Dict[str,
|
self.__producer_pairs_df: Dict[str,
|
||||||
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
||||||
@@ -64,10 +66,19 @@ class DataProvider:
|
|||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
"""
|
"""
|
||||||
Limit analyzed dataframe to max specified index.
|
Limit analyzed dataframe to max specified index.
|
||||||
|
Only relevant in backtesting.
|
||||||
:param limit_index: dataframe index.
|
:param limit_index: dataframe index.
|
||||||
"""
|
"""
|
||||||
self.__slice_index = limit_index
|
self.__slice_index = limit_index
|
||||||
|
|
||||||
|
def _set_dataframe_max_date(self, limit_date: datetime):
|
||||||
|
"""
|
||||||
|
Limit infomrative dataframe to max specified index.
|
||||||
|
Only relevant in backtesting.
|
||||||
|
:param limit_date: "current date"
|
||||||
|
"""
|
||||||
|
self.__slice_date = limit_date
|
||||||
|
|
||||||
def _set_cached_df(
|
def _set_cached_df(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -356,6 +367,11 @@ class DataProvider:
|
|||||||
# Get historical OHLCV data (cached on disk).
|
# Get historical OHLCV data (cached on disk).
|
||||||
timeframe = timeframe or self._config['timeframe']
|
timeframe = timeframe or self._config['timeframe']
|
||||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
|
# Cut date to timeframe-specific date.
|
||||||
|
# This is necessary to prevent lookahead bias in callbacks through informative pairs.
|
||||||
|
if self.__slice_date:
|
||||||
|
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
|
||||||
|
data = data.loc[data['date'] < cutoff_date]
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -369,13 +369,14 @@ class Backtesting:
|
|||||||
# Cleanup from prior runs
|
# Cleanup from prior runs
|
||||||
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
|
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
|
||||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
|
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
|
||||||
# Trim startup period from analyzed dataframe
|
|
||||||
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
|
||||||
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
|
||||||
# Update dataprovider cache
|
# Update dataprovider cache
|
||||||
self.dataprovider._set_cached_df(
|
self.dataprovider._set_cached_df(
|
||||||
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
|
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
|
||||||
|
|
||||||
|
# Trim startup period from analyzed dataframe
|
||||||
|
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
||||||
|
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
||||||
|
|
||||||
# Create a copy of the dataframe before shifting, that way the entry signal/tag
|
# Create a copy of the dataframe before shifting, that way the entry signal/tag
|
||||||
# remains on the correct candle for callbacks.
|
# remains on the correct candle for callbacks.
|
||||||
df_analyzed = df_analyzed.copy()
|
df_analyzed = df_analyzed.copy()
|
||||||
@@ -1196,7 +1197,8 @@ class Backtesting:
|
|||||||
|
|
||||||
row_index += 1
|
row_index += 1
|
||||||
indexes[pair] = row_index
|
indexes[pair] = row_index
|
||||||
self.dataprovider._set_dataframe_max_index(row_index)
|
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time)
|
||||||
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
||||||
|
|
||||||
@@ -1229,12 +1231,14 @@ class Backtesting:
|
|||||||
is_first = True
|
is_first = True
|
||||||
current_time_det = current_time
|
current_time_det = current_time
|
||||||
for det_row in detail_data[HEADERS].values.tolist():
|
for det_row in detail_data[HEADERS].values.tolist():
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time_det)
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
det_row, pair, current_time_det, end_date,
|
det_row, pair, current_time_det, end_date,
|
||||||
open_trade_count_start, trade_dir, is_first)
|
open_trade_count_start, trade_dir, is_first)
|
||||||
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
||||||
is_first = False
|
is_first = False
|
||||||
else:
|
else:
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time)
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
row, pair, current_time, end_date,
|
row, pair, current_time, end_date,
|
||||||
open_trade_count_start, trade_dir)
|
open_trade_count_start, trade_dir)
|
||||||
|
|||||||
@@ -129,9 +129,14 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
|
|||||||
default_conf["runmode"] = RunMode.BACKTEST
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.BACKTEST
|
assert dp.runmode == RunMode.BACKTEST
|
||||||
assert isinstance(dp.get_pair_dataframe(
|
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||||
"UNITTEST/BTC", timeframe, candle_type=candle_type), DataFrame)
|
assert isinstance(df, DataFrame)
|
||||||
# assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
assert len(df) == 3 # ohlcv_history mock has just 3 rows
|
||||||
|
|
||||||
|
dp._set_dataframe_max_date(ohlcv_history.iloc[-1]['date'])
|
||||||
|
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||||
|
assert isinstance(df, DataFrame)
|
||||||
|
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
|
||||||
|
|
||||||
|
|
||||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_prev_date
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import LocalTrade, Trade
|
from freqtrade.persistence import LocalTrade, Trade
|
||||||
@@ -1135,6 +1135,12 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
|||||||
assert candle_date == current_time
|
assert candle_date == current_time
|
||||||
# These asserts don't properly raise as they are nested,
|
# These asserts don't properly raise as they are nested,
|
||||||
# therefore we increment count and assert for that.
|
# therefore we increment count and assert for that.
|
||||||
|
df = dp.get_pair_dataframe(pair, backtesting.strategy.timeframe)
|
||||||
|
prior_time = timeframe_to_prev_date(backtesting.strategy.timeframe,
|
||||||
|
candle_date - timedelta(seconds=1))
|
||||||
|
assert prior_time == df.iloc[-1].squeeze()['date']
|
||||||
|
assert df.iloc[-1].squeeze()['date'] < current_time
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
||||||
@@ -1353,11 +1359,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
|
|
||||||
# Cached data correctly removed amounts
|
# Cached data correctly removed amounts
|
||||||
offset = 1 if tres == 0 else 0
|
offset = 1 if tres == 0 else 0
|
||||||
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
|
removed_candles = len(data[pair]) - offset
|
||||||
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
||||||
assert len(
|
assert len(
|
||||||
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
||||||
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
) == len(data['NXT/BTC']) - 1
|
||||||
|
|
||||||
backtesting.strategy.max_open_trades = 1
|
backtesting.strategy.max_open_trades = 1
|
||||||
backtesting.config.update({'max_open_trades': 1})
|
backtesting.config.update({'max_open_trades': 1})
|
||||||
|
|||||||
Reference in New Issue
Block a user