Convert backtesting rows to Tuples for performance gains
This commit is contained in:
parent
5d3a67d324
commit
cf2ae788d7
@ -28,6 +28,15 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Indexes for backtest tuples
|
||||||
|
DATE_IDX = 0
|
||||||
|
BUY_IDX = 1
|
||||||
|
OPEN_IDX = 2
|
||||||
|
CLOSE_IDX = 3
|
||||||
|
SELL_IDX = 4
|
||||||
|
LOW_IDX = 5
|
||||||
|
HIGH_IDX = 6
|
||||||
|
|
||||||
|
|
||||||
class BacktestResult(NamedTuple):
|
class BacktestResult(NamedTuple):
|
||||||
"""
|
"""
|
||||||
@ -115,7 +124,7 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
Load strategy into backtesting
|
Load strategy into backtesting
|
||||||
"""
|
"""
|
||||||
self.strategy = strategy
|
self.strategy: IStrategy = strategy
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-sell is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
@ -147,12 +156,14 @@ class Backtesting:
|
|||||||
|
|
||||||
return data, timerange
|
return data, timerange
|
||||||
|
|
||||||
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
|
||||||
"""
|
"""
|
||||||
Helper function to convert a processed dataframes into lists for performance reasons.
|
Helper function to convert a processed dataframes into lists for performance reasons.
|
||||||
|
|
||||||
Used by backtest() - so keep this optimized for performance.
|
Used by backtest() - so keep this optimized for performance.
|
||||||
"""
|
"""
|
||||||
|
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||||
|
# and eventually change the constants for indexes at the top
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||||
data: Dict = {}
|
data: Dict = {}
|
||||||
# Create dict with data
|
# Create dict with data
|
||||||
@ -172,10 +183,10 @@ class Backtesting:
|
|||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
data[pair] = [x for x in df_analyzed.itertuples(index=False)]
|
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
"""
|
"""
|
||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
@ -186,12 +197,12 @@ class Backtesting:
|
|||||||
return trade.stop_loss
|
return trade.stop_loss
|
||||||
elif sell.sell_type == (SellType.ROI):
|
elif sell.sell_type == (SellType.ROI):
|
||||||
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
|
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
|
||||||
if roi is not None:
|
if roi is not None and roi_entry is not None:
|
||||||
if roi == -1 and roi_entry % self.timeframe_min == 0:
|
if roi == -1 and roi_entry % self.timeframe_min == 0:
|
||||||
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
|
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
|
||||||
# If that entry is a multiple of the timeframe (so on candle open)
|
# If that entry is a multiple of the timeframe (so on candle open)
|
||||||
# - we'll use open instead of close
|
# - we'll use open instead of close
|
||||||
return sell_row.open
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||||
close_rate = - (trade.open_rate * roi + trade.open_rate *
|
close_rate = - (trade.open_rate * roi + trade.open_rate *
|
||||||
@ -199,31 +210,29 @@ class Backtesting:
|
|||||||
|
|
||||||
if (trade_dur > 0 and trade_dur == roi_entry
|
if (trade_dur > 0 and trade_dur == roi_entry
|
||||||
and roi_entry % self.timeframe_min == 0
|
and roi_entry % self.timeframe_min == 0
|
||||||
and sell_row.open > close_rate):
|
and sell_row[OPEN_IDX] > close_rate):
|
||||||
# new ROI entry came into effect.
|
# new ROI entry came into effect.
|
||||||
# use Open rate if open_rate > calculated sell rate
|
# use Open rate if open_rate > calculated sell rate
|
||||||
return sell_row.open
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
# Use the maximum between close_rate and low as we
|
# Use the maximum between close_rate and low as we
|
||||||
# cannot sell outside of a candle.
|
# cannot sell outside of a candle.
|
||||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||||
return max(close_rate, sell_row.low)
|
return max(close_rate, sell_row[LOW_IDX])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This should not be reached...
|
# This should not be reached...
|
||||||
return sell_row.open
|
return sell_row[OPEN_IDX]
|
||||||
else:
|
else:
|
||||||
return sell_row.open
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: Trade, sell_row) -> Optional[BacktestResult]:
|
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]:
|
||||||
"""
|
|
||||||
sell_row is a named tuple with attributes for date, buy, open, close, sell, low, high.
|
|
||||||
"""
|
|
||||||
|
|
||||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
|
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
|
||||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||||
|
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
trade_dur = int((sell_row.date - trade.open_date).total_seconds() // 60)
|
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
|
|
||||||
return BacktestResult(pair=trade.pair,
|
return BacktestResult(pair=trade.pair,
|
||||||
@ -232,7 +241,7 @@ class Backtesting:
|
|||||||
open_date=trade.open_date,
|
open_date=trade.open_date,
|
||||||
open_rate=trade.open_rate,
|
open_rate=trade.open_rate,
|
||||||
open_fee=self.fee,
|
open_fee=self.fee,
|
||||||
close_date=sell_row.date,
|
close_date=sell_row[DATE_IDX],
|
||||||
close_rate=closerate,
|
close_rate=closerate,
|
||||||
close_fee=self.fee,
|
close_fee=self.fee,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
@ -242,8 +251,8 @@ class Backtesting:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_left_open(self, open_trades: Dict[str, List],
|
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
|
||||||
data: Dict[str, DataFrame]) -> List[BacktestResult]:
|
data: Dict[str, List[Tuple]]) -> List[BacktestResult]:
|
||||||
"""
|
"""
|
||||||
Handling of left open trades at the end of backtesting
|
Handling of left open trades at the end of backtesting
|
||||||
"""
|
"""
|
||||||
@ -254,17 +263,17 @@ class Backtesting:
|
|||||||
sell_row = data[pair][-1]
|
sell_row = data[pair][-1]
|
||||||
trade_entry = BacktestResult(pair=trade.pair,
|
trade_entry = BacktestResult(pair=trade.pair,
|
||||||
profit_percent=trade.calc_profit_ratio(
|
profit_percent=trade.calc_profit_ratio(
|
||||||
rate=sell_row.open),
|
rate=sell_row[OPEN_IDX]),
|
||||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]),
|
||||||
open_date=trade.open_date,
|
open_date=trade.open_date,
|
||||||
open_rate=trade.open_rate,
|
open_rate=trade.open_rate,
|
||||||
open_fee=self.fee,
|
open_fee=self.fee,
|
||||||
close_date=sell_row.date,
|
close_date=sell_row[DATE_IDX],
|
||||||
close_rate=sell_row.open,
|
close_rate=sell_row[OPEN_IDX],
|
||||||
close_fee=self.fee,
|
close_fee=self.fee,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
trade_duration=int((
|
trade_duration=int((
|
||||||
sell_row.date - trade.open_date
|
sell_row[DATE_IDX] - trade.open_date
|
||||||
).total_seconds() // 60),
|
).total_seconds() // 60),
|
||||||
open_at_end=True,
|
open_at_end=True,
|
||||||
sell_reason=SellType.FORCE_SELL
|
sell_reason=SellType.FORCE_SELL
|
||||||
@ -323,7 +332,7 @@ class Backtesting:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Waits until the time-counter reaches the start of the data for this pair.
|
# Waits until the time-counter reaches the start of the data for this pair.
|
||||||
if row.date > tmp:
|
if row[DATE_IDX] > tmp:
|
||||||
continue
|
continue
|
||||||
indexes[pair] += 1
|
indexes[pair] += 1
|
||||||
|
|
||||||
@ -333,14 +342,14 @@ class Backtesting:
|
|||||||
if ((position_stacking or len(open_trades[pair]) == 0)
|
if ((position_stacking or len(open_trades[pair]) == 0)
|
||||||
and max_open_trades > 0 and open_trade_count_start < max_open_trades
|
and max_open_trades > 0 and open_trade_count_start < max_open_trades
|
||||||
and tmp != end_date
|
and tmp != end_date
|
||||||
and row.buy == 1 and row.sell != 1):
|
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
|
||||||
# Enter trade
|
# Enter trade
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=row.open,
|
open_rate=row[OPEN_IDX],
|
||||||
open_date=row.date,
|
open_date=row[DATE_IDX],
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round(stake_amount / row.open, 8),
|
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
|
@ -94,14 +94,14 @@ class Hyperopt:
|
|||||||
|
|
||||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||||
self.backtesting.strategy.advise_indicators = \
|
self.backtesting.strategy.advise_indicators = ( # type: ignore
|
||||||
self.custom_hyperopt.populate_indicators # type: ignore
|
self.custom_hyperopt.populate_indicators) # type: ignore
|
||||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||||
self.backtesting.strategy.advise_buy = \
|
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
||||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||||
self.backtesting.strategy.advise_sell = \
|
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||||
self.custom_hyperopt.populate_sell_trend # type: ignore
|
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
||||||
|
|
||||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if self.config.get('use_max_market_positions', True):
|
||||||
@ -508,16 +508,16 @@ class Hyperopt:
|
|||||||
params_details = self._get_params_details(params_dict)
|
params_details = self._get_params_details(params_dict)
|
||||||
|
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
self.backtesting.strategy.minimal_roi = \
|
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.backtesting.strategy.advise_buy = \
|
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
||||||
|
|
||||||
if self.has_space('sell'):
|
if self.has_space('sell'):
|
||||||
self.backtesting.strategy.advise_sell = \
|
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||||
|
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||||
|
Loading…
Reference in New Issue
Block a user