Merge pull request #4454 from freqtrade/backtest_compound_speed
Backtest compound, wallet, ...
This commit is contained in:
@@ -17,17 +17,18 @@ from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.persistence import LocalTrade, PairLocks, Trade
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -114,6 +115,8 @@ class Backtesting:
|
||||
if self.config.get('enable_protections', False):
|
||||
self.protections = ProtectionManager(self.config)
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
# Load one (first) strategy
|
||||
@@ -124,7 +127,7 @@ class Backtesting:
|
||||
PairLocks.use_db = True
|
||||
Trade.use_db = True
|
||||
|
||||
def _set_strategy(self, strategy):
|
||||
def _set_strategy(self, strategy: IStrategy):
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
"""
|
||||
@@ -171,10 +174,8 @@ class Backtesting:
|
||||
PairLocks.use_db = False
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
Trade.use_db = False
|
||||
if enable_protections:
|
||||
# Reset persisted data - used for protections only
|
||||
PairLocks.reset_locks()
|
||||
Trade.reset_trades()
|
||||
PairLocks.reset_locks()
|
||||
Trade.reset_trades()
|
||||
|
||||
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
|
||||
"""
|
||||
@@ -203,10 +204,10 @@ class Backtesting:
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
# (Looping Pandas is slow.)
|
||||
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
|
||||
data[pair] = df_analyzed.values.tolist()
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
@@ -246,24 +247,48 @@ class Backtesting:
|
||||
else:
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]:
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
|
||||
sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
if sell.sell_flag:
|
||||
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
trade.close_date = sell_row[DATE_IDX]
|
||||
trade.sell_reason = sell.sell_type
|
||||
trade.sell_reason = sell.sell_type.value
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
trade.close(closerate, show_msg=False)
|
||||
return trade
|
||||
|
||||
return None
|
||||
|
||||
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
|
||||
data: Dict[str, List[Tuple]]) -> List[Trade]:
|
||||
def _enter_trade(self, pair: str, row: List, max_open_trades: int,
|
||||
open_trade_count: int) -> Optional[LocalTrade]:
|
||||
try:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(
|
||||
pair, max_open_trades - open_trade_count, None)
|
||||
except DependencyException:
|
||||
return None
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
|
||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||
# Enter trade
|
||||
trade = LocalTrade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
open_date=row[DATE_IDX],
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
exchange='backtesting',
|
||||
)
|
||||
return trade
|
||||
return None
|
||||
|
||||
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
|
||||
data: Dict[str, List[Tuple]]) -> List[LocalTrade]:
|
||||
"""
|
||||
Handling of left open trades at the end of backtesting
|
||||
"""
|
||||
@@ -274,13 +299,15 @@ class Backtesting:
|
||||
sell_row = data[pair][-1]
|
||||
|
||||
trade.close_date = sell_row[DATE_IDX]
|
||||
trade.sell_reason = SellType.FORCE_SELL
|
||||
trade.sell_reason = SellType.FORCE_SELL.value
|
||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
||||
trade.is_open = True
|
||||
trades.append(trade)
|
||||
# Deepcopy object to have wallets update correctly
|
||||
trade1 = deepcopy(trade)
|
||||
trade1.is_open = True
|
||||
trades.append(trade1)
|
||||
return trades
|
||||
|
||||
def backtest(self, processed: Dict, stake_amount: float,
|
||||
def backtest(self, processed: Dict,
|
||||
start_date: datetime, end_date: datetime,
|
||||
max_open_trades: int = 0, position_stacking: bool = False,
|
||||
enable_protections: bool = False) -> DataFrame:
|
||||
@@ -292,7 +319,6 @@ class Backtesting:
|
||||
Avoid extensive logging in this method and functions it calls.
|
||||
|
||||
:param processed: a processed dictionary with format {pair, data}
|
||||
:param stake_amount: amount to use for each trade
|
||||
:param start_date: backtesting timerange start datetime
|
||||
:param end_date: backtesting timerange end datetime
|
||||
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
|
||||
@@ -300,11 +326,7 @@ class Backtesting:
|
||||
:param enable_protections: Should protections be enabled?
|
||||
:return: DataFrame with trades (results of backtesting)
|
||||
"""
|
||||
logger.debug(f"Run backtest, stake_amount: {stake_amount}, "
|
||||
f"start_date: {start_date}, end_date: {end_date}, "
|
||||
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
|
||||
)
|
||||
trades: List[Trade] = []
|
||||
trades: List[LocalTrade] = []
|
||||
self.prepare_backtest(enable_protections)
|
||||
|
||||
# Use dict of lists with data for performance
|
||||
@@ -315,7 +337,7 @@ class Backtesting:
|
||||
indexes: Dict = {}
|
||||
tmp = start_date + timedelta(minutes=self.timeframe_min)
|
||||
|
||||
open_trades: Dict[str, List] = defaultdict(list)
|
||||
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
@@ -346,28 +368,18 @@ class Backtesting:
|
||||
and tmp != end_date
|
||||
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
|
||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
|
||||
# Enter trade
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
open_date=row[DATE_IDX],
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
)
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behaviour - not sure if this is correct
|
||||
# Prevents buying if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
open_trade_count += 1
|
||||
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
||||
open_trades[pair].append(trade)
|
||||
Trade.trades.append(trade)
|
||||
trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start)
|
||||
if trade:
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behaviour - not sure if this is correct
|
||||
# Prevents buying if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
open_trade_count += 1
|
||||
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||
open_trades[pair].append(trade)
|
||||
LocalTrade.trades.append(trade)
|
||||
|
||||
for trade in open_trades[pair]:
|
||||
# since indexes has been incremented before, we need to go one step back to
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occured
|
||||
@@ -384,6 +396,7 @@ class Backtesting:
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
self.wallets.update()
|
||||
|
||||
return trade_list_to_dataframe(trades)
|
||||
|
||||
@@ -417,7 +430,6 @@ class Backtesting:
|
||||
# Execute backtest and store results
|
||||
results = self.backtest(
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date.datetime,
|
||||
end_date=max_date.datetime,
|
||||
max_open_trades=max_open_trades,
|
||||
@@ -428,7 +440,8 @@ class Backtesting:
|
||||
self.all_results[self.strategy.get_strategy_name()] = {
|
||||
'results': results,
|
||||
'config': self.strategy.config,
|
||||
'locks': PairLocks.locks,
|
||||
'locks': PairLocks.get_all_locks(),
|
||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||
'backtest_start_time': int(backtest_start_time.timestamp()),
|
||||
'backtest_end_time': int(backtest_end_time.timestamp()),
|
||||
}
|
||||
|
Reference in New Issue
Block a user